article · ~5 min · live data

The swimmer's safety gauge

Chicago's Park District publishes hourly water-quality telemetry from buoys at six Lake Michigan beaches. The 24-hour-lab E. coli result that triggers the city's official advisory lags the swimmer's afternoon by a full day. What you see when you arrive is a statistical forecast — not a measurement.

The Tuesday of July 14, 2015 was a hot afternoon at Montrose Beach. The water-quality buoy a few meters offshore was logging readings every hour. For most of the morning the turbidity sensor — a measure of how cloudy the water is, in nephelometric turbidity units (NTU) — sat quietly under 3 NTU. Then, mid-afternoon, the sensor blinked a value of 27.8 NTU: roughly ten times the morning baseline. A thunderstorm cell, a passing boat wake, a sediment plume from the Chicago River outflow — the sensor doesn't say which. It just says the water clouded up.

What the chart says is "the water clouded up sharply, then settled." What the chart can't say is "this water was unsafe to swim in." Turbidity is a proxy for biological contamination — cloudy water often contains more bacteria — but it isn't itself a pathogen count. The city's official beach-water advisory comes from a separate laboratory test that incubates a sample for 24 hours and counts E. coli colonies. The advisory you see when you arrive at the beach is yesterday's swimmers' lab result, statistically projected forward.

Six beaches, one summer

In summer 2015 — the last full season when Chicago's automated beach-sensor network ran at its peak six-buoy coverage — every one of the six in-water sensors was reporting hourly. Across the May-through-September window, the lakefront's pattern looked like this:

The biggest beach-to-beach surprise: Osterman Beach, on the far North Side, averaged six times the turbidity of Ohio Street Beach downtown over the same summer. Both are on the same lake. The difference is partly bathymetry (Osterman is in shallower, more turbid water near the river-mouth outflow) and partly sensor placement. Same lake; different lakefronts.

The shape of a season

Daily averages flatten the hourly noise into a per-beach shape. Below: one sparkline per beach, May 1 through September 30, 2015. The peaks line up — when one beach's sensor spikes, the others tend to spike too. The lake breathes together. The slow drift in mean turbidity across the season is a different signal: sediment patterns shift with the wind and the river outflow over the summer.

The network that shrank

The 2015 record is the last summer in which all six water sensors reported. Bars below count the number of distinct beaches reporting at least one hourly reading per calendar year. From a 2015 peak of six, the network dropped to one by 2019, briefly re-expanded to seven beaches in early 2025, then collapsed back to one by March of that year.

The city's beach-water advisory program still operates — predictions and lab tests continue to drive the daily green-flag / red-flag decision posted at each beach kiosk. But the real-time, in-water telemetry that once supplemented those forecasts has narrowed to a single sensor at Ohio Street Beach. The "safety gauge" promised by hourly automated monitoring is, at this point, mostly aspirational.

The dataset is small and friendly to ad-hoc analysis — about 0 rows across the full archive. Slice it yourself via the dataset page, or pull the raw SoQL from data.cityofchicago.org / qmqz-2xku.

View underlying data

Summer 2015 averages per beach (May 1 – Oct 1)

BeachHourly rowsAvg turbidity (NTU)Avg temp (°C)Avg wave height (m)

Distinct beaches reporting per year (all-time)

YearBeachesRows

Data Sources

Hourly buoy readings: Chicago Park District beach-water sensors (qmqz-2xku). Four SoQL queries run at runtime through the worker proxy: hourlyForBeach() for the Montrose 2015-07-14 sparkline, seasonalAverages() for the map, dailyAveragesBySummer() for the per-beach small-multiples, and coverageByYear() for the network-collapse bars. Source: VIEWS in src/lib/data/datasets/chicago-beach-water-quality.ts.

Sensor coordinates: Sensor Locations (g3ip-u8rb). The six water-sensor lat/lon pairs are hard-coded into the adapter as BEACH_SENSOR_COORDS since they're stable.

Methodology: Data fetched at runtime through the Cloudflare Worker proxy at /api/socrata. The proxy KV-caches aggregates for one hour. Sensor coverage has been narrowing since 2016; the editorial frame uses summer 2015 because it's the last full-season multi-sensor record.