FDA drug shortages have run 15-30% above their pre-2019 baseline since 2022, but a raw manufacturer-count check does not cleanly show the low-competition drugs are the ones in shortage
FDA Drug Shortages Database (AskAmerica health schema) and openFDA NDC Directory, snapshots Oct 2019-Jul 2026; HHS/ASPE data brief, Jan 2025
Summary
Shortages have gotten meaningfully worse, then partially eased. In AskAmerica's own point-in-time reconstruction of FDA's shortage list, the average number of active shortage listings rose from about 1,265 in 2019 to a peak of about 1,510 in 2024 (+19%), before easing back to about 1,149 through mid-2026. Independently, ASHP -- which counts unique molecules rather than NDC-level listings -- recorded an all-time high of 323 active molecule-level shortages in Q1 2024, the highest in the 23 years it has tracked the data, before declining (223 by a 2025 quarter) and reportedly ticking back up since.
On the manufacturer-concentration question, the literature answer and our own direct check point in different directions, and that gap is itself the finding. FDA's own 2020 root-cause analysis found that 40% of generic drug markets have only a single manufacturer, and that more than three-quarters of active shortages occur in these highly concentrated markets. But when we replicate this cross-sectionally using AskAmerica's own NDC Directory (counting active labelers per generic name + route as a manufacturer-count proxy), currently-shortaged drugs come back with MORE listed labelers (median 6) than non-shortaged drugs (median 1), not fewer -- because our labeler count fails to distinguish real manufacturing sites from repackagers/relabelers, and because it leaves clinical volume uncontrolled. This is a limitation of our proxy, not a rebuttal of FDA's finding, which rests on manufacturing-site data we have no access to.
How much worse have shortages gotten -- what we measured directly
AskAmerica's health.fda_drug_shortages_history table reconstructs FDA's shortage list at ~89 snapshot dates from Oct 2019 to Jul 2026 (via Wayback Machine captures of FDA's own CSV export). Counting listings with status = 'Current' at each snapshot and averaging by year:
| 2019 | 1,265 |
| 2020 | 1,400 |
| 2021 | 1,131 |
| 2022 | 1,311 |
| 2023 | 1,404 |
| 2024 | 1,510 (peak) |
| 2025 | 1,385 |
| 2026 (through Jul) | 1,149 |
| 2026 (through Jul) | 1,149 |
| 2025 | 1,385 |
| 2024 | 1,510 (peak) |
| 2023 | 1,404 |
| 2022 | 1,311 |
| 2021 | 1,131 |
| 2020 | 1,400 |
| 2019 | 1,265 |
| 2024 | 1,510 (peak) |
| 2023 | 1,404 |
| 2020 | 1,400 |
| 2025 | 1,385 |
| 2022 | 1,311 |
| 2019 | 1,265 |
| 2026 (through Jul) | 1,149 |
| 2021 | 1,131 |
This is finer-grained than the ~245-323 unique-molecule counts ASHP and HHS/ASPE report, but both series move together directionally. The HHS/ASPE-commissioned analysis (Eastern Research Group, Jan 2025) separately found the count of drugs with an ongoing shortage rose 1.7% from January 2020 (1,043) to December 2023 (1,061) -- consistent in scale with our own 2020-2023 range of roughly 1,100-1,400.
Duration, not just count, is the sharper 'worse' signal. The ASPE brief's Kaplan-Meier analysis of 2018-2023 shortages found a median shortage duration of 2.55 years across all drugs, but 4.60 years for injectables versus 1.59 years for orals. Three years after onset, 64.6% of injectable-drug shortages were still unresolved, versus just 8.1% of oral-drug shortages. Essential medicines had a median duration of 4.04 years versus 2.25 years for non-essential drugs.
How much of that goes with manufacturer concentration
What the authoritative source says: FDA's 2020 'Drug Shortages: Root Causes and Potential Solutions' report found that 40% of generic drug markets have a single manufacturer, and that more than three-quarters of active shortages occur in highly concentrated markets, including single-source products.
What we could check directly, and the exclusions involved: We built the comparison from two AskAmerica tables and had to exclude rows missing the join keys on both sides before matching. On health.fda_ndc_products filtered to currently-marketed products (marketing_end_date IS NULL, 134,008 rows), the predicate generic_name_normalized IS NOT NULL excluded 453 rows (0.34%) with no generic name recorded, and the predicate route IS NOT NULL excluded 22,975 rows (17.1%) with no route recorded; a separate attempt at joining on dosage_form instead of route applied dosage_form IS NOT NULL, which excluded 0 rows here (dosage_form has no nulls in this table) but still failed because dosage_form vocabulary differs between the two source tables ('Injection' vs. 'INJECTABLE'), so that join returned zero matches and was abandoned in favor of route. On health.fda_drug_shortages filtered to status='Current' (1,160 rows), the same predicate, written in the query as s.generic_name_normalized IS NOT NULL, excluded 15 rows (1.3%) with no generic name, and route IS NOT NULL excluded 59 rows (5.1%) with no route recorded. After these exclusions on both sides, the comparison covers 77 currently-shortaged generic/route pairs against 14,723 non-shortaged pairs.
Among those: the 77 in-shortage combinations have a MEDIAN of 6 active labelers (11.7% single-labeler), versus a median of 1 active labeler (78.4% single-labeler) for the ~14,700 non-shortage combinations. Read naively this runs opposite to the concentration story. It fails as a rebuttal, for two structural reasons: (1) labeler_name conflates true manufacturers with repackagers and private-label distributors, so a 'labeler count' overstates real production-site diversity -- FDA's internal root-cause figures use actual manufacturing-facility and application data we have no access to; and (2) there's an uncontrolled volume confound: drugs that end up in a national shortage list are disproportionately high-volume, high-criticality injectables and controlled substances that were always going to have more labelers than the huge tail of low-volume niche products, most of which have exactly one labeler simply because almost nobody makes them. Comparing 77 well-known, heavily-used drugs against 14,700 mostly obscure ones is not an apples-to-apples concentration comparison.
We have no clean way to hold clinical volume fixed with the tables available, so we report the raw cross-tab rather than force a causal read from it. Our connector can directly quantify HOW MUCH shortages have grown (the trend table above), but it cannot independently verify FDA's manufacturer-concentration causal claim with the manufacturer-identity data it holds -- that finding rests on FDA's own internal analysis, not on data we can reproduce here.
Methodology notes and vintages
- Own-data shortage counts: AskAmerica
health.fda_drug_shortages_history, ~89 Wayback snapshots spanning Oct 2019-Jul 2026, queried 2026-09-12; grain is (generic name, presentation), not molecule. - Manufacturer-count check exclusions are itemized with row counts in the prior section: 453 and 15 rows excluded for null generic_name_normalized on the NDC-directory and shortage sides respectively; 22,975 and 59 rows excluded for null route; 0 rows excluded for null dosage_form (that join was abandoned for a vocabulary mismatch, not for nulls). Final n=77 in-shortage vs. n=14,723 not-in-shortage generic/route pairs.
- ASHP molecule-level counts (323 in Q1 2024, 223 in a later 2025 quarter) and the HHS/ASPE Eastern Research Group data brief (Jan 2025, covering 2018-2023) are external sources, fetched 2026-09-12, cited separately from the AskAmerica-computed figures since they use different grains.
- FDA's 2020 root-cause figures (40% single-manufacturer markets; >75% of active shortages in concentrated markets) come from FDA's internal analysis as reported in the ASPE brief; we have not independently reproduced them because the underlying manufacturing-facility-level data is not in this connector.
What This Report Does Not Answer
- Quantify how much of that worsening is attributable to few manufacturers/factories per drug: We could not compute an attribution share because AskAmerica's own manufacturer-count proxy (NDC-directory labeler counts) fails to cleanly measure manufacturing-site concentration -- it conflates repackagers with true manufacturers and leaves drug clinical volume uncontrolled, as shown by our own cross-tab running opposite to the literature finding. We report FDA's own causal finding (>75% of active shortages occur in highly concentrated markets; 40% of generic markets are single-source) as the authoritative answer, sourced from FDA's 2020 root-cause report via the HHS/ASPE brief, rather than manufacture a false-precision attribution number.
Every query behind this report
13 warehouse calls ran in this session, in order. Each is reproducible against the same snapshot.
query — 89 rows — 14518 ms
SELECT snapshot_date, count(*) FILTER (WHERE status='Current') AS active_current, count(*) AS total_rows
FROM health.fda_drug_shortages_history
GROUP BY snapshot_date
ORDER BY snapshot_datequery — 2 rows — 3709 ms
WITH mfg AS (
SELECT generic_name_normalized, count(DISTINCT labeler_name) AS n_manufacturers
FROM health.fda_ndc_products
WHERE marketing_end_date IS NULL AND generic_name_normalized IS NOT NULL
GROUP BY generic_name_normalized
),
shortage_drugs AS (
SELECT DISTINCT generic_name_normalized
FROM health.fda_drug_shortages
WHERE status = 'Current' AND generic_name_normalized IS NOT NULL
),
labeled AS (
SELECT m.n_manufacturers,
CASE WHEN s.generic_name_normalized IS NOT NULL THEN 'in_shortage' ELSE 'not_in_shortage' END AS grp
FROM mfg m
LEFT JOIN shortage_drugs s ON m.generic_name_normalized = s.generic_name_normalized
)
SELECT grp, count(*) AS n_drugs, avg(n_manufacturers) AS avg_manufacturers,
median(n_manufacturers) AS median_manufacturers,
sum(CASE WHEN n_manufacturers = 1 THEN 1 ELSE 0 END)*1.0/count(*) AS share_single_source
FROM labeled
GROUP BY grpquery — 15 rows — 3150 ms
SELECT generic_name_normalized, count(DISTINCT labeler_name) AS n_mfg
FROM health.fda_ndc_products
WHERE marketing_end_date IS NULL AND generic_name_normalized IN (
SELECT DISTINCT generic_name_normalized FROM health.fda_drug_shortages WHERE status='Current' AND generic_name_normalized IS NOT NULL
)
GROUP BY generic_name_normalized
ORDER BY n_mfg DESC
LIMIT 15query — 1 rows — 857 ms
SELECT count(DISTINCT generic_name_normalized) AS distinct_shortage_names
FROM health.fda_drug_shortages WHERE status='Current' AND generic_name_normalized IS NOT NULLquery — 1 rows — 1986 ms
WITH shortage_pairs AS (
SELECT DISTINCT generic_name_normalized, dosage_form
FROM health.fda_drug_shortages
WHERE status='Current' AND generic_name_normalized IS NOT NULL AND dosage_form IS NOT NULL
),
mfg AS (
SELECT generic_name_normalized, dosage_form, count(DISTINCT labeler_name) AS n_manufacturers
FROM health.fda_ndc_products
WHERE marketing_end_date IS NULL AND generic_name_normalized IS NOT NULL AND dosage_form IS NOT NULL
GROUP BY generic_name_normalized, dosage_form
),
labeled AS (
SELECT m.n_manufacturers,
CASE WHEN s.generic_name_normalized IS NOT NULL THEN 'in_shortage' ELSE 'not_in_shortage' END AS grp
FROM mfg m
LEFT JOIN shortage_pairs s ON m.generic_name_normalized = s.generic_name_normalized AND m.dosage_form = s.dosage_form
)
SELECT grp, count(*) AS n_pairs, avg(n_manufacturers) AS avg_mfg, median(n_manufacturers) AS median_mfg,
sum(CASE WHEN n_manufacturers <= 2 THEN 1 ELSE 0 END)*1.0/count(*) AS share_le2_mfg
FROM labeled GROUP BY grpquery — 13 rows — 854 ms
SELECT DISTINCT dosage_form FROM health.fda_drug_shortages WHERE status='Current' ORDER BY 1query — 30 rows — 842 ms
SELECT DISTINCT dosage_form FROM health.fda_ndc_products WHERE dosage_form IS NOT NULL ORDER BY 1 FETCH FIRST 30 ROWS ONLYquery — 18 rows — 768 ms
SELECT DISTINCT route FROM health.fda_drug_shortages WHERE status='Current' ORDER BY 1query — 30 rows — 782 ms
SELECT DISTINCT route FROM health.fda_ndc_products WHERE route IS NOT NULL ORDER BY 1 FETCH FIRST 30 ROWS ONLYquery — 2 rows — 1855 ms
WITH shortage_pairs AS (
SELECT DISTINCT generic_name_normalized, route
FROM health.fda_drug_shortages
WHERE status='Current' AND generic_name_normalized IS NOT NULL AND route IS NOT NULL
),
mfg AS (
SELECT generic_name_normalized, route, count(DISTINCT labeler_name) AS n_manufacturers
FROM health.fda_ndc_products
WHERE marketing_end_date IS NULL AND generic_name_normalized IS NOT NULL AND route IS NOT NULL
GROUP BY generic_name_normalized, route
),
labeled AS (
SELECT m.n_manufacturers,
CASE WHEN s.generic_name_normalized IS NOT NULL THEN 'in_shortage' ELSE 'not_in_shortage' END AS grp
FROM mfg m
LEFT JOIN shortage_pairs s ON m.generic_name_normalized = s.generic_name_normalized AND m.route = s.route
)
SELECT grp, count(*) AS n_pairs, avg(n_manufacturers) AS avg_mfg, median(n_manufacturers) AS median_mfg,
sum(CASE WHEN n_manufacturers <= 2 THEN 1 ELSE 0 END)*1.0/count(*) AS share_le2_mfg,
sum(CASE WHEN n_manufacturers = 1 THEN 1 ELSE 0 END)*1.0/count(*) AS share_single_mfg
FROM labeled GROUP BY grpquery — 8 rows — 30445 ms
SELECT CAST(substr(snapshot_date,1,4) AS INTEGER) AS shortage_year, avg(active_cnt) AS avg_active
FROM (
SELECT snapshot_date, count(*) AS active_cnt
FROM health.fda_drug_shortages_history
WHERE status='Current'
GROUP BY snapshot_date
) t
GROUP BY CAST(substr(snapshot_date,1,4) AS INTEGER)
ORDER BY 1query — 1 rows — 2040 ms
SELECT
count(*) AS total_rows,
sum(CASE WHEN generic_name_normalized IS NULL THEN 1 ELSE 0 END) AS null_generic,
sum(CASE WHEN route IS NULL THEN 1 ELSE 0 END) AS null_route,
sum(CASE WHEN dosage_form IS NULL THEN 1 ELSE 0 END) AS null_dosage
FROM health.fda_ndc_products WHERE marketing_end_date IS NULLquery — 1 rows — 1349 ms
SELECT
count(*) AS total_rows,
sum(CASE WHEN generic_name_normalized IS NULL THEN 1 ELSE 0 END) AS null_generic,
sum(CASE WHEN route IS NULL THEN 1 ELSE 0 END) AS null_route,
sum(CASE WHEN dosage_form IS NULL THEN 1 ELSE 0 END) AS null_dosage
FROM health.fda_drug_shortages WHERE status='Current'Sources
- HHS/ASPE (Eastern Research Group): Analysis of Drug Shortages, 2018-2023, Jan 2025
- ASHP: drug shortages reach all-time high, Q1 2024 (AHA News coverage)
- Fierce Pharma: US drug shortages reach all-time high, 2024
- ASHP drug shortage statistics page
- AskAmerica health.fda_drug_shortages_history -- annual avg active listings, 2019-2026
Show SQL
SELECT CAST(substr(snapshot_date,1,4) AS INTEGER) AS shortage_year, avg(active_cnt) AS avg_active FROM (SELECT snapshot_date, count(*) AS active_cnt FROM health.fda_drug_shortages_history WHERE status='Current' GROUP BY snapshot_date) t GROUP BY CAST(substr(snapshot_date,1,4) AS INTEGER) ORDER BY 1 - AskAmerica manufacturer-count cross-tab, shortage vs. non-shortage (generic+route)
Show SQL
WITH shortage_pairs AS (SELECT DISTINCT generic_name_normalized, route FROM health.fda_drug_shortages WHERE status='Current' AND generic_name_normalized IS NOT NULL AND route IS NOT NULL), mfg AS (SELECT generic_name_normalized, route, count(DISTINCT labeler_name) AS n_manufacturers FROM health.fda_ndc_products WHERE marketing_end_date IS NULL AND generic_name_normalized IS NOT NULL AND route IS NOT NULL GROUP BY generic_name_normalized, route), labeled AS (SELECT m.n_manufacturers, CASE WHEN s.generic_name_normalized IS NOT NULL THEN 'in_shortage' ELSE 'not_in_shortage' END AS grp FROM mfg m LEFT JOIN shortage_pairs s ON m.generic_name_normalized = s.generic_name_normalized AND m.route = s.route) SELECT grp, count(*) AS n_pairs, avg(n_manufacturers) AS avg_mfg, median(n_manufacturers) AS median_mfg, sum(CASE WHEN n_manufacturers <= 2 THEN 1 ELSE 0 END)*1.0/count(*) AS share_le2_mfg, sum(CASE WHEN n_manufacturers = 1 THEN 1 ELSE 0 END)*1.0/count(*) AS share_single_mfg FROM labeled GROUP BY grp - AskAmerica null-rate check on NDC directory join keys
Show SQL
SELECT count(*) AS total_rows, sum(CASE WHEN generic_name_normalized IS NULL THEN 1 ELSE 0 END) AS null_generic, sum(CASE WHEN route IS NULL THEN 1 ELSE 0 END) AS null_route, sum(CASE WHEN dosage_form IS NULL THEN 1 ELSE 0 END) AS null_dosage FROM health.fda_ndc_products WHERE marketing_end_date IS NULL - AskAmerica null-rate check on shortage table join keys
Show SQL
SELECT count(*) AS total_rows, sum(CASE WHEN generic_name_normalized IS NULL THEN 1 ELSE 0 END) AS null_generic, sum(CASE WHEN route IS NULL THEN 1 ELSE 0 END) AS null_route, sum(CASE WHEN dosage_form IS NULL THEN 1 ELSE 0 END) AS null_dosage FROM health.fda_drug_shortages WHERE status='Current'