Beyond U-3.
How the headline unemployment rate systematically understates labor market stress — and what to watch instead.
The thesis
The headline U-3 unemployment rate (currently 4.3%, April 2026) excludes three groups that materially change the picture of labor market health:
- Discouraged workers who stopped looking for jobs.
- Marginally attached workers who want work but haven't searched recently.
- Involuntary part-time workers who want full-time employment but can't find it.
The BLS U-6 rate, which includes all three, currently sits at 8.2% — nearly double U-3. This project builds the data infrastructure to track that gap historically, contextualize it against recessions, and monitor the forward-looking indicators that turn before U-3 does.
U-3 vs U-6, 1994–2026
Monthly seasonally-adjusted rates from FRED. Shaded bands mark NBER recessions. The persistent gap between the two lines is the “hidden” labor stress U-3 fails to capture.
Notice the COVID spike: U-3 hit 14.7%, U-6 hit 22.8%. The gap widens most in stress periods — exactly when policymakers most need accurate signals. Data: FRED series UNRATE, U6RATE.
Key findings
The hidden labor gap is not stable — it widens in stress
| Period | Avg U-3/U-6 gap | Months |
|---|---|---|
| Great Recession (Dec '07 — Jun '09) | 5.37 pts | 19 |
| COVID Shock (Feb — Apr 2020) | 5.33 pts | 3 |
| Normal Periods | 4.42 pts | ~340 |
| Dot-com Recession (Mar — Nov '01) | 4.18 pts | 9 |
Computed via SQL with a CASE-based recession classifier — see analysis_queries.sql →
Eight leading indicators — current state
Not every indicator is flashing red. The thesis is not that recession is certain — it is that U-3 alone gives an incomplete picture, and the indicators underneath show more stress than the headline suggests.
Architecture
Every piece is automated. A single Python entry-point reads 14 FRED series, resamples mixed-frequency data (daily/weekly/monthly → monthly), computes derived metrics, and loads everything into PostgreSQL via SQLAlchemy. Windows Task Scheduler kicks it off on the 5th of each month, two days after the typical BLS Employment Situation release.
FRED API (14 series) | v python/fred_pipeline.py |- fredapi pulls all series |- resamples to monthly |- computes U3/U6 gap, temp-help % off all-time peak |- bulk-inserts via SQLAlchemy | v PostgreSQL 18 (labor_dashboard) |- unemployment_measures |- labor_force_participation |- epop |- leading_indicators | v CSV exports --> Tableau Public (2 dashboards)
Schema & SQL
Four normalized tables. Each pipeline run truncates and re-inserts so the snapshot is end-to-end consistent with the latest FRED release.
CREATE TABLE unemployment_measures (
id SERIAL PRIMARY KEY,
date DATE NOT NULL,
u3_rate DECIMAL(5,2),
u4_rate DECIMAL(5,2),
u5_rate DECIMAL(5,2),
u6_rate DECIMAL(5,2)
);
CREATE TABLE leading_indicators (
id SERIAL PRIMARY KEY,
date DATE NOT NULL,
temp_help DECIMAL(10,2),
temp_help_pct_decline DECIMAL(5,2),
quits_rate DECIMAL(5,2),
saving_rate DECIMAL(5,2),
jobless_claims DECIMAL(10,0),
yield_curve DECIMAL(6,3),
consumer_credit DECIMAL(10,2),
cc_delinquency DECIMAL(5,2),
prime_age_lfpr DECIMAL(5,2)
);
The query behind the headline finding
SELECT
CASE
WHEN date BETWEEN '2007-12-01' AND '2009-06-30' THEN 'Great Recession'
WHEN date BETWEEN '2020-02-01' AND '2020-04-30' THEN 'COVID Shock'
WHEN date BETWEEN '2001-03-01' AND '2001-11-30' THEN 'Dot-com Recession'
ELSE 'Normal Period'
END AS period,
ROUND(AVG(u6_rate - u3_rate), 2) AS avg_gap_pts,
COUNT(*) AS months
FROM unemployment_measures
GROUP BY 1
ORDER BY avg_gap_pts DESC;
The 14 FRED series
UNRATE · U-3 Unemployment RateU4RATE · + discouraged workersU5RATE · + marginally attachedU6RATE · + involuntary part-timeCIVPART · Labor Force Participation RateEMRATIO · Employment-to-Population RatioTEMPHELPS · Temp Help Services EmploymentJTSQUR · JOLTS Quits RatePSAVERT · Personal Saving RateICSA · Initial Jobless Claims (wkly)T10Y2Y · 10Y–2Y Treasury Spread (daily)TOTALSL · Total Consumer Credit OutstandingDRCCLACBS · Credit Card Delinquency RateLNS11300060 · Prime-Age (25–54) LFPR