How to Rank US Dams by Reservoir Storage
On this page
Problem statement
Hoover Dam's Lake Mead and Glen Canyon's Lake Powell are the giants among American reservoirs, and a few dozen big federal dams hold most of the water stored behind all the country's dams. The National Inventory of Dams records storage for every dam in acre-feet, the volume that covers an acre a foot deep, about 1,233 cubic metres. Ranking them should be a sort, but the top of the list is not what you expect: a navigation lock is listed with a Great Lake behind it, and some reservoirs appear several times. This page ranks storage, shows how concentrated it is, and converts the units.
Quick answer
Sort by nidStorage, keep one row per NID id, and convert acre-feet to cubic kilometres:
AF_TO_KM3 = 1233.48 / 1e9
one = nid.sort_values("isAssociatedStructureId").drop_duplicates("nidId")
top = one.nlargest(8, "nidStorage")[["name", "state", "riverName", "nidStorage"]].assign(km3=lambda d: d.nidStorage * AF_TO_KM3)
print(top.round(1).to_string(index=False))
name state riverName nidStorage km3
Soo Locks Michigan St Mary S 277540000.0 342.3
Hoover Dam Nevada Colorado River 30237000.0 37.3
Garrison Dam North Dakota Missouri River 26000000.0 32.1
Glen Canyon Dam Arizona Colorado River 25025826.0 30.9
Oahe Dam South Dakota Missouri River 23600000.0 29.1
Fort Peck Dam Montana Missouri River 19100000.0 23.6
Grand Coulee Dam Washington Columbia River 9715346.0 12.0
Herbert Hoover Dike - Ciz D Florida Kissimmee River Basin 8519000.0 10.5
After the Soo Locks come the familiar giants: Hoover Dam's Lake Mead with 37.3 km³, then Garrison, Glen Canyon, Oahe and Fort Peck, four of them on the Missouri and Colorado. Grand Coulee holds 12.0 km³. Eighth is a segment of the Herbert Hoover Dike, which rings Lake Okeechobee and is listed as seven rows under one id, each carrying the whole lake's storage. Keeping one row per id removes the others, and the storage-counting page deals with the repeats that have ids of their own.
Concentration and units
1. A navigation lock and a Great Lake
soo = one[one.name == "Soo Locks"].iloc[0]
print(f"{soo['name']}: {soo.nidStorage:,.0f} acre-feet = {soo.nidStorage * AF_TO_KM3:,.0f} km³, purpose {soo.primaryPurposeId}, height {soo.nidHeight:.0f} ft")
Soo Locks: 277,540,000 acre-feet = 342 km³, purpose Navigation, height 17 ft
The Soo Locks, 17 feet high, are listed with 342 km³ of storage, about nine times Hoover Dam's. They and the control works beside them sit at the outlet of Lake Superior, a natural lake whose level they regulate by a few feet. The figure isn't water a dam has stored in any useful sense, so leave the Soo Locks out of any total or ranking of reservoirs.
2. How concentrated storage is
s = one[one.name != "Soo Locks"].nidStorage.sort_values(ascending=False)
share = s.cumsum() / s.sum()
print(f"dams with storage: {(s > 0).sum():,}; the largest 100 hold {share.iloc[99]:.1%}; the largest 1,000 hold {share.iloc[999]:.1%}")
print(f"median storage: {s[s > 0].median():,.0f} acre-feet")
dams with storage: 90,681; the largest 100 hold 49.1%; the largest 1,000 hold 87.3%
median storage: 134 acre-feet
Storage is extremely concentrated. Of 90,681 dams with storage, the largest 100 hold 49.1% of the total and the largest 1,000 hold 87.3%, while the median dam holds 134 acre-feet, about 0.17 million m³, a farm pond. Totals are a story about a few hundred dams. Counts are a story about tens of thousands of small ones.
3. Normal and maximum storage
r = (one.normalStorage / one.maxStorage).where(one.maxStorage > 0)
print(f"normal as a share of maximum storage: median {r.median():.2f}; normal above maximum: {(r > 1).sum()}; negative normal storage: {(one.normalStorage < 0).sum()}")
normal as a share of maximum storage: median 0.55; normal above maximum: 349; negative normal storage: 3
Normal storage, the level the reservoir is usually kept at, is typically 55% of the maximum, which includes flood space above it. 349 dams have normal storage above their maximum and 3 have negative normal storage, which are both data errors. nidStorage takes the larger of the two, so it describes a full reservoir.
Storage by state and purpose
The data used on this page
The US Army Corps of Engineers' National Inventory of Dams (NID), the whole inventory as a GeoPackage from its API, downloaded on 19 October 2026 and saved on this site, because the inventory is updated as states and agencies send revisions. Heights are in feet, storage in acre-feet and areas in acres, as the NID gives them.
from pathlib import Path
import zipfile
import geopandas as gpd
import numpy as np
import pandas as pd
import requests
Path("data").mkdir(exist_ok=True)
# US Army Corps of Engineers, National Inventory of Dams: the whole inventory as a GeoPackage, and the Census
# Bureau's 2024 cartographic boundary counties. Saved 19 October 2026, because the inventory is updated continuously.
# Live: https://nid.sec.usace.army.mil/api/nation/gpkg
ARCHIVE = "usace-nid-2026-10-19.zip"
if not Path(f"data/{ARCHIVE}").exists():
Path(f"data/{ARCHIVE}").write_bytes(requests.get(f"https://data.spatialworkflow.io/{ARCHIVE}", timeout=900).content)
Z = zipfile.ZipFile(f"data/{ARCHIVE}")
if not Path("data/nid.gpkg").exists():
Z.extract("nid.gpkg", "data")
NUMERIC = ["damHeight", "hydraulicHeight", "structuralHeight", "nidHeight", "damLength", "volume", "yearCompleted",
"nidStorage", "maxStorage", "normalStorage", "surfaceArea", "drainageArea", "maxDischarge"]
nid = gpd.read_file("data/nid.gpkg")
nid[NUMERIC] = nid[NUMERIC].apply(pd.to_numeric, errors="coerce") # the GeoPackage stores them as text
print(f"National Inventory of Dams: {len(nid):,} rows, {nid.nidId.nunique():,} NID ids")
National Inventory of Dams: 92,626 rows, 91,840 NID ids
Example 1: by state
by = (one[one.name != "Soo Locks"].groupby("state").nidStorage.sum() * AF_TO_KM3).sort_values(ascending=False)
print(by.head(6).round(1).to_string())
state
Texas 119.2
North Dakota 80.4
California 57.6
Arizona 53.2
Montana 49.9
Oklahoma 47.1
By state, Texas stores the most, 119.2 km³ across thousands of dams. North Dakota is second with 80.4 km³, almost all in Lake Sakakawea behind Garrison Dam, and Arizona and Montana owe theirs to a few great federal reservoirs.
Example 2: by purpose
print((one[one.name != "Soo Locks"].groupby("primaryPurposeId").nidStorage.sum() * AF_TO_KM3).sort_values(ascending=False).head(6).round(1).to_string())
primaryPurposeId
Flood Risk Reduction 523.6
Hydroelectric 277.5
Water Supply 66.4
Irrigation 47.8
Recreation 46.3
Navigation 42.2
By primary purpose, flood risk reduction holds the most storage, 523.6 km³, then hydropower with 277.5 km³. These totals still contain repeated segments such as the Herbert Hoover Dike's, so treat them as upper bounds until reservoirs are counted once.
Internal links
- Storage counted many times — why one row per id is not quite enough
- Dams by purpose and owner — the purposes behind the storage
- Numbers stored as text — why a sort on storage goes wrong
- Reading the inventory — the columns and units