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.

Horizontal bars of the twelve largest reservoirs in cubic kilometres: Hoover Dam 37.3, Garrison 32.1, Glen Canyon 30.9, Oahe 29.1 and Fort Peck 23.6. Horizontal bars of the twelve largest reservoirs in cubic kilometres: Hoover Dam 37.3, Garrison 32.1, Glen Canyon 30.9, Oahe 29.1 and Fort Peck 23.6.
The largest reservoirs by storage.

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.

Cumulative share of storage against dams ranked by storage on a log axis, reaching 49.1% at the largest 100 and 87.3% at the largest 1,000. Cumulative share of storage against dams ranked by storage on a log axis, reaching 49.1% at the largest 100 and 87.3% at the largest 1,000.
A few hundred dams hold most of the water.

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.

Horizontal bars of storage by state in cubic kilometres, led by Texas 119.2, North Dakota 80.4 and California 57.6. Horizontal bars of storage by state in cubic kilometres, led by Texas 119.2, North Dakota 80.4 and California 57.6.
Reservoir storage by state.

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.