Toxic Release Totals Explained: Air, Water, Land and Off-Site
On this page
Problem statement
"Total releases" in the Toxics Release Inventory is not emissions. It adds air, water, underground injection, chemicals placed in landfills and surface impoundments on site, and chemicals sent off site for disposal. For mines the land part is waste rock and tailings, millions of pounds of metals that stay where they are put. And releases are only a small part of the toxic chemical waste facilities manage, most of which is recycled or treated. This page breaks the 2024 total into its routes, shows which columns hold the numbers, and compares releases with all production waste.
Quick answer
Split the total into air, water, underground injection, land on site and off site:
air = pounds(t24, "5.1 - FUGITIVE AIR") + pounds(t24, "5.2 - STACK AIR")
water = pounds(t24, "5.3 - WATER")
under = pounds(t24, "5.4.1 - UNDERGROUND CL I") + pounds(t24, "5.4.2 - UNDERGROUND C II-V")
onsite = pounds(t24, "ON-SITE RELEASE TOTAL")
tot = pd.Series({"air (stack and fugitive)": air.sum(), "water": water.sum(), "underground injection": under.sum(),
"land on site": (onsite - air - water - under).sum(), "off site": pounds(t24, "OFF-SITE RELEASE TOTAL").sum()}) / 1e6
print(tot.round(1).to_string())
print(f"sum {tot.sum():.1f}M lb; TOTAL RELEASES column {pounds(t24).sum() / 1e6:.1f}M lb")
air (stack and fugitive) 530.3
water 188.7
underground injection 284.6
land on site 1838.7
off site 448.2
sum 3290.5M lb; TOTAL RELEASES column 3290.5M lb
Of 3,290.5 million lb released in 2024, 1,838.7 million went onto land at the facility, 530.3 million to air, 284.6 million into underground injection wells, 188.7 million to water, and 448.2 million were sent off site for disposal. The routes add up exactly to the TOTAL RELEASES column.
The columns behind the total
1. Which land columns hold numbers
for c in [c for c in t24.columns if c.startswith("5.5")]:
print(f"{c:28} {pounds(t24, c).sum() / 1e6:10.1f}M lb, non-zero rows {(t24[c] > 0).sum():,}")
5.5.1 - LANDFILLS 0.0M lb, non-zero rows 0
5.5.1A - RCRA C LANDFILL 141.3M lb, non-zero rows 855
5.5.1B - OTHER LANDFILLS 182.1M lb, non-zero rows 2,470
5.5.2 - LAND TREATMENT 14.4M lb, non-zero rows 482
5.5.3 - SURFACE IMPNDMNT 0.0M lb, non-zero rows 0
5.5.3A - RCRA SURFACE IM 0.3M lb, non-zero rows 45
5.5.3B - OTHER SURFACE I 772.9M lb, non-zero rows 1,486
5.5.4 - OTHER DISPOSAL 727.7M lb, non-zero rows 1,889
The parent columns 5.5.1 - LANDFILLS and 5.5.3 - SURFACE IMPNDMNT are empty; the numbers are in their sub-columns. Other surface impoundments hold 772.9 million lb and "other disposal" 727.7 million, the two largest land routes.
2. Releases are not the same as waste
print(f"production-related waste (8.1-8.7): {pounds(t24, 'PRODUCTION WSTE (8.1-8.7)').sum() / 1e9:.2f} billion lb")
for c in ["8.4 - RECYCLING ON SITE", "8.6 - TREATMENT ON SITE", "8.2 - ENERGY RECOVER ON", "8.1 - RELEASES"]:
print(f"{c:28} {pounds(t24, c).sum() / 1e9:.2f}")
production-related waste (8.1-8.7): 41.10 billion lb
8.4 - RECYCLING ON SITE 25.01
8.6 - TREATMENT ON SITE 7.09
8.2 - ENERGY RECOVER ON 2.38
8.1 - RELEASES 0.00
Production-related waste was 41.10 billion lb in 2024, more than twelve times the releases. 25.01 billion was recycled on site and 7.09 billion treated on site. The 8.1 - RELEASES column is empty in this file; its sub-columns 8.1A to 8.1D carry the numbers.
3. Each sector's route
t24["air"] = pounds(t24, "5.1 - FUGITIVE AIR") + pounds(t24, "5.2 - STACK AIR")
t24["water"] = pounds(t24, "5.3 - WATER")
t24["all"] = pounds(t24)
sec = t24.groupby("INDUSTRY SECTOR")[["all", "air", "water"]].sum()
sec = sec.assign(air_share=sec.air / sec["all"], water_share=sec.water / sec["all"]).nlargest(6, "all")
print(sec[["all", "air_share", "water_share"]].assign(all=lambda d: (d["all"] / 1e6).round(1)).round(3).to_string())
all air_share water_share
INDUSTRY SECTOR
Metal Mining 1345.4 0.001 0.001
Chemicals 445.8 0.382 0.058
Primary Metals 344.5 0.053 0.048
Hazardous Waste 205.0 0.001 0.000
Electric Utilities 191.1 0.243 0.014
Natural Gas Processing 159.9 0.029 0.000
Metal mining's 1,345.4 million lb are 0.1% air and 0.1% water. Chemical plants send 38.2% of their releases to air and electric utilities 24.3%. A sector's total says little about what its neighbours breathe.
A single facility, and the off-site part
The data used on this page
The US EPA's Toxics Release Inventory basic data files for 2014 and 2024, public domain, downloaded on 16 October 2026 and saved on this site, because EPA revises past years when facilities resubmit. With them come the Census Bureau's Vintage 2025 county population estimates and 2024 county boundaries.
from pathlib import Path
import re
import zipfile
import geopandas as gpd
import numpy as np
import pandas as pd
import requests
Path("data").mkdir(exist_ok=True)
# EPA Toxics Release Inventory basic data files, reporting years 2014 and 2024, all states, xz-compressed and
# otherwise unchanged; Census Vintage 2025 county estimates and 2024 cartographic boundary counties.
# Downloaded 16 October 2026 and saved here, because EPA revises past years.
# Live: https://data.epa.gov/efservice/downloads/tri/mv_tri_basic_download/2024_US/csv (and 2014_US)
ARCHIVE = "epa-tri-2014-2024-2026-10-16.zip"
if not Path(f"data/{ARCHIVE}").exists():
Path(f"data/{ARCHIVE}").write_bytes(requests.get(f"https://data.spatialworkflow.io/{ARCHIVE}", timeout=600).content)
Z = zipfile.ZipFile(f"data/{ARCHIVE}")
def tri(year):
"""One year's TRI file: a row per facility and chemical, columns numbered '1. YEAR' to '122. ...'."""
d = pd.read_csv(Z.open(f"tri_{year}_us.csv.xz"), compression="xz", dtype=str, low_memory=False)
d.columns = [re.sub(r"^\d+\.\s*", "", c) for c in d.columns]
amounts = list(d.columns[d.columns.get_loc("5.1 - FUGITIVE AIR"):d.columns.get_loc("8.8 - ONE-TIME RELEASE") + 1])
d[amounts + ["LATITUDE", "LONGITUDE"]] = d[amounts + ["LATITUDE", "LONGITUDE"]].apply(pd.to_numeric, errors="coerce")
return d
def pounds(d, col="TOTAL RELEASES"):
"""A quantity in pounds: dioxin and dioxin-like compounds are reported in grams."""
return d[col].where(d["UNIT OF MEASURE"] == "Pounds", d[col] / 453.59237)
t24 = tri(2024)
pop = pd.read_csv(Z.open("co-est2025-alldata.csv"), encoding="latin-1", dtype={"STATE": str, "COUNTY": str})
pop = pop[pop.SUMLEV == 50].assign(GEOID=lambda p: p.STATE + p.COUNTY)
Path("data/cb_2024_us_county_20m.zip").write_bytes(Z.read("cb_2024_us_county_20m.zip"))
counties = gpd.read_file("data/cb_2024_us_county_20m.zip")
print(f"TRI 2024: {len(t24):,} facility-chemical rows from {t24.TRIFD.nunique():,} facilities; {pounds(t24).sum() / 1e9:.2f} billion lb released")
TRI 2024: 77,295 facility-chemical rows from 21,482 facilities; 3.29 billion lb released
Example 1: one mine's report
red = t24[t24["FACILITY NAME"].str.contains("RED DOG", na=False)]
print(red.groupby("CHEMICAL")["TOTAL RELEASES"].sum().nlargest(4).div(1e6).round(1).to_string())
print(f"Red Dog: {pounds(red).sum() / 1e6:.1f}M lb, {pounds(red).sum() / pounds(t24).sum():.1%} of the US total")
CHEMICAL
Zinc compounds 380.8
Lead And Lead Compounds 319.4
Manganese And Manganese Compounds 15.9
Chromium and Chromium Compounds(except for chromite ore mined in the Transvaal Region) 2.6
Red Dog: 723.9M lb, 22.0% of the US total
Red Dog's report is mostly zinc and lead compounds, 380.8 and 319.4 million lb of its 723.9 million lb, which alone are 22.0% of the US total.
Example 2: where off-site releases go
off = list(t24.columns[t24.columns.get_loc("6.2 - M10"):t24.columns.get_loc("OFF-SITE RELEASE TOTAL")])
o = pd.Series({c: pounds(t24, c).sum() for c in off}).sort_values(ascending=False) / 1e6
print(o.head(5).round(1).to_string())
6.2 - M64 184.8
6.2 - M65 50.3
6.2 - M41 36.2
6.2 - M82 35.6
6.2 - M90 25.5
Off-site releases go mainly to landfills at other facilities: codes M64 and M65, 184.8 and 50.3 million lb, are the largest.
Internal links
- Read the TRI files β the 122 columns
- The few facilities behind most releases β the mines in the total
- Air releases per resident β the air route alone
- Pounds and grams β the unit every sum here depends on