Architecture
How Lock Wait works
The Army Corps of Engineers publishes, for every navigation lock it runs, a list of every tow that arrived and how long it waited. Each list covers only the last thirty days. Lock Wait asks for all of it on a schedule, keeps every answer exactly as received, and builds these pages from the archive. This page explains each piece, with live numbers from the running system.
1. Why an archive is needed
Ask the Corps for a lock's traffic and it returns the last thirty days. Ask again tomorrow and the window has moved: one new day at the front, one day gone from the back. Nothing older is published. Each bar below is one request we made for , drawn from the earliest to the latest arrival it returned.
The heavy line underneath is what the archive now holds for this lock: the union of every window. It grows by a day each day for as long as the collector runs. The same is true of lock status, which the Corps overwrites every fifteen minutes and does not publish at all as history.
2. The pipeline
Five stages, each simple enough to check by hand. Nothing is parsed until the raw response is safely on disk, and nothing on disk is ever rewritten.
3. The source
The Corps' public lock application at ndc.ops.usace.army.mil is an Oracle APEX app. Behind it sits a documented JSON API (the Lock Performance Monitoring System, LPMS) with an OpenAPI catalogue. We use five parts of it.
| Endpoint | Returns | Window | We ask |
|---|---|---|---|
lock_status_report | Every reporting lock: tows pending, 4-hour average delay, stoppages, gauges, weather, notices, lockmaster notes | Now only | Every 15 minutes, one request |
traffic_report | Each lockage at one lock: vessel, direction, barges, arrival, start and end of lockage, hazardous-cargo flag | 30 days | Daily, all 192 locks |
lock_queue_json | The same lockages with the vessel's radio identity (MMSI) | 30 days | Daily, all 192 locks |
lookups/* | Reference tables: lock numbers, river codes, chambers, stoppage reasons | Current | Daily |
monthly_tons_report | Commodity tonnage per lock per month | Monthly | Not yet |
4. The collector
One Python file using only the standard library, run as a systemd service with Restart=always. It does three things on a loop.
- Status, every 15 minutes. One request returns every reporting lock. This is the only record of live conditions that exists outside the Corps.
- A full sweep, every 24 hours. Lookups, then traffic and queue for each of the 192 locks, paced under the rate limit.
- A note of where it is. A small
state.jsonrecords when the last sweep started and finished. A restart resumes the schedule; a sweep cut off part-way reruns.
Every response is written gzipped, byte for byte, before anything reads it, and one line is appended to a manifest. Failures are recorded the same way as successes. This is the manifest line for the most recent status request:
Every request in the last 48 hours
5. Storage
No database. The archive is a directory of gzipped files, one per request, named by endpoint, date and time:
/var/lib/corps-locks/
manifest.jsonl one line per request: time, url, status, bytes, sha256, rows
state.json last sweep started / finished, last status pull
raw/
lock_status_report/2026-10-05/all_093905Z.json.gz
traffic_report/2026-10-04/MI-27_150212Z.json.gz
lock_queue_json/… lock_numbers/… river_codes/…
reference/
vessel_operators.csv USCG official number → operating company
lock_coords.csv BTS lock locations, river miles, chamber sizes
6. The builder
Every 15 minutes a second script reads the whole archive and writes the JSON these pages use. It runs from scratch each time, so the site is always a pure function of the raw files.
- Merge overlapping windows. Each lockage appears in up to thirty daily pulls. It is identified by lock, vessel number and arrival time and kept once.
- Fold split flotillas. A tow too long for the chamber goes through in several cuts, each reported as a row. The builder keeps the first start, the last end and the largest barge count.
- Measure the wait as start of lockage minus arrival. Values below zero or above two weeks are dropped as reporting errors.
- Join three outside sources. Vessel numbers are Coast Guard official numbers, which the Corps' vessel-operator file and FCC ship-station licences both carry. Lock locations and dimensions come from the BTS National Transportation Atlas.
- Fold status snapshots into a time series per lock.
- Write and swap. Output goes to a sibling directory and replaces the live one in a single rename, so a reader never sees a half-written file.
7. The site
Four static pages and one stylesheet. There is no build step and no framework. Each page fetches its JSON and draws with d3: a map of every lock, a small-multiples table of all 192, a page per lock that plots every lockage, and this one.
| File | Contains |
|---|---|
data/index.json | One summary per lock: totals, daily counts, median and 90th-percentile wait, latest status |
data/locks/<id>.json | Every lockage, the status series, operators, a weekday-by-hour arrival grid |
data/health.json | Everything on this page |
8. Hosting and operations
| Piece | What it is | Runs |
|---|---|---|
| Server | One DigitalOcean droplet: 1 vCPU, 512 MB memory, 10 GB disk, New York. $4 a month. | Always |
corps-locks.service | The collector | Always, restarts on failure |
river-locks-build.timer | The builder | At :02, :17, :32 and :47 past each hour |
| Caddy | Web server; obtains and renews the HTTPS certificate on its own | Always |
| Health check | collect.py health fails if no status snapshot has landed in 45 minutes | Hourly, from a second machine that also keeps a full copy of the archive |
9. What we found in the data
10. How it was built
- Screened the idea as a data business: search demand, buyers, and whether the history really disappears.
- Found the JSON API behind the Corps' lock app and confirmed the 30-day window.
- First collector run on a laptop. The first sweep finished at 15:36: 192 of 192 locks, no failures, 88 minutes.
- Made the collector restart itself after a crash or reboot; tested by killing it.
- Status every 15 minutes, matching the Corps' own refresh.
- Found a three-hour gap: the laptop had slept. Moved collection to a $4 server with a health check by 21:44.
- Joined vessels to operators through Coast Guard numbers and FCC licences.
- Built the site, matched all 192 locks to locations, published at lockwait.com.
11. Limits
- History begins on 4 October 2026 for status and on about 4 September 2026 for lockages. A zero before those dates means we were not recording, not that nothing happened.
- "Wait" is our measure: arrival to start of lockage. It may differ from the Corps' own delay statistics.
- Operators come from the Corps' 2022 vessel file and FCC licences. A boat sold since then may show its previous operator.
- Cargo, shipper and destination for each lockage are confidential under federal rules (33 CFR 209.320) and do not appear anywhere here.
- Lock times are local lock time as reported, with a zone label; we do not adjust for daylight saving.
Archive contains 192 lock records; 173 have recorded traffic. Data built 2026-10-05T14:17:12Z. Coverage varies by lock.