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.

EndpointReturnsWindowWe ask
lock_status_reportEvery reporting lock: tows pending, 4-hour average delay, stoppages, gauges, weather, notices, lockmaster notesNow onlyEvery 15 minutes, one request
traffic_reportEach lockage at one lock: vessel, direction, barges, arrival, start and end of lockage, hazardous-cargo flag30 daysDaily, all 192 locks
lock_queue_jsonThe same lockages with the vessel's radio identity (MMSI)30 daysDaily, all 192 locks
lookups/*Reference tables: lock numbers, river codes, chambers, stoppage reasonsCurrentDaily
monthly_tons_reportCommodity tonnage per lock per monthMonthlyNot 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.json records 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.

  1. 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.
  2. 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.
  3. Measure the wait as start of lockage minus arrival. Values below zero or above two weeks are dropped as reporting errors.
  4. 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.
  5. Fold status snapshots into a time series per lock.
  6. 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.

FileContains
data/index.jsonOne summary per lock: totals, daily counts, median and 90th-percentile wait, latest status
data/locks/<id>.jsonEvery lockage, the status series, operators, a weekday-by-hour arrival grid
data/health.jsonEverything on this page

8. Hosting and operations

PieceWhat it isRuns
ServerOne DigitalOcean droplet: 1 vCPU, 512 MB memory, 10 GB disk, New York. $4 a month.Always
corps-locks.serviceThe collectorAlways, restarts on failure
river-locks-build.timerThe builderAt :02, :17, :32 and :47 past each hour
CaddyWeb server; obtains and renews the HTTPS certificate on its ownAlways
Health checkcollect.py health fails if no status snapshot has landed in 45 minutesHourly, 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

    1. Screened the idea as a data business: search demand, buyers, and whether the history really disappears.
    2. Found the JSON API behind the Corps' lock app and confirmed the 30-day window.
    3. First collector run on a laptop. The first sweep finished at 15:36: 192 of 192 locks, no failures, 88 minutes.
    4. Made the collector restart itself after a crash or reboot; tested by killing it.
    5. Status every 15 minutes, matching the Corps' own refresh.
    6. Found a three-hour gap: the laptop had slept. Moved collection to a $4 server with a health check by 21:44.
    7. Joined vessels to operators through Coast Guard numbers and FCC licences.
    8. 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.