How viewers' browsers keep New York's cameras honest
At 08:15 on a weekday Broadway @ 45 St has the Times Square bowtie filling from the north, and it hands you a new frame every two seconds. That refresh rate is the fastest of any street camera we carry in the city, and the picture you are looking at never passes through our servers. Your browser fetches it from New York City DOT directly, the way a person on the city's own site would.
We did not choose that for speed. It is the only arrangement that works for this host, and the reason it works is that the people watching are a better network than we are.
Two ways a picture gets to you
Every camera on the site loads down one of two paths. On the proxy path your browser asks us for /api/feed/<id>, our server in Ashburn fetches the upstream image and streams it back to you. On the direct path your browser loads the operator's URL itself, in a plain <img> tag, and we are not in the loop at all. Which one a camera uses is decided per camera after ingest: image and mjpeg feeds can go direct with no cross-origin headers at all, whilst m3u8 and mp4 need a true Access-Control-Allow-Origin: * before hls.js can touch them, and an iframe camera is always effectively direct because the browser is framing the provider's own page.
23,147 of the 142,646 live cameras on the site load direct today, counted from our fleet table at 13:06 UTC on 2026-09-29 (the fleet, by country and source). The other path is the default, and for most sources it is the better one, because we can cache, we can set the headers a fussy upstream wants, and we can check the picture ourselves.
webcams.nyctmc.org is in the small set where the default is wrong. It serves a residential browser in under half a second and it will not serve our datacenter at all. I pulled the city's own list from this sandbox at 13:07 UTC today and it answered in 0.83 seconds with 971 cameras, every one flagged online: 377 in Manhattan, 209 in Queens, 206 in Brooklyn, 101 on Staten Island, 78 in the Bronx. A single image off that host, the Times Square one, came back as 25,581 bytes of JPEG in 0.42 seconds. From Fly the same request times out. So we stopped asking, and the reader's browser asks instead.
The cameras that arrangement keeps on the site
All of these are New York City DOT, all of them refresh every two seconds, and all of them are loading in your browser from the city rather than from us.
- Central Park West @ 65 St looks up the park wall at the Tavern on the Green turn. Late October is the one to watch, when the ginkgoes along that stretch go yellow inside about a week.
- Rockefeller Plz @ 48 St sits between 5th and 6th on the block that closes for the tree. From the first week of December it is the cheapest way to see how bad the crowd is before you set off.
- FDR Dr @ Brooklyn Bridge catches the southbound queue for the bridge approach. At two seconds a frame you can watch a single truck cause the back-up.
- Holland Tunnel is the city's "Canal St @ Greenwich St ( Holland Tunnel )", pointed at the portal approach on the Manhattan side.
- Central Park West @ 86 St is quiet and green and almost never has anything happening on it, which after an hour of Midtown junctions is the point.
Our Manhattan page carries these alongside the 30-second stills from NY 511 and the streaming views from other operators, which is why you will sometimes see the same junction twice at two different refresh rates. The two-second one is the city's.
The browsers do the checking
Handing the fetch to the reader creates an obvious problem. Our nightly health check works by fetching every camera and looking at the bytes, and on a host that refuses us there are no bytes to look at. For a long stretch that check returned a cheerful alive: true for cameras it had never actually seen, which put a pass in the history for a test that did not run.
The fix shipped on 2026-09-08 and it is one line of principle: recording a verdict you did not observe is strictly worse than recording none. The check now returns unverified rather than a result, and every caller branches on that first. last_checked is touched so the camera's turn in the cycle advances, active is left exactly as it was in both directions, and no history row is written at all. A camera on one of these hosts has an honest gap in its record instead of a run of invented passes.
What fills the gap is the readers. When a direct <img> fails in someone's browser, the page sends us a beacon saying so, and a sweep every 30 minutes looks at what came in. It deactivates a camera only when at least 8 failures have arrived from at least 3 distinct IP addresses inside a 72-hour window with not one successful direct load against it, and only while that camera's source is demonstrably still working for other people at the same time. It never touches a camera that is not on one of these blocked hosts, because everywhere else the health check has a real verdict of its own. Each sweep is capped at 50 deactivations.
That second condition is the whole safety property, and it is worth being explicit about why. If the city ever started firewalling ordinary residential users too, every NYC DOT camera would fail at once, from everywhere, and a naive rule would take the lot down in half an hour. Under this rule the source's success ratio collapses at the same moment, the guard trips, and the sweep deactivates nothing at all. The signal that says "this one camera is broken" and the signal that says "something happened to the whole host" look identical from one viewer, so we make the fleet decide.
There is one thing a beacon cannot say. It reports that a picture failed to load; it never reports that the operator took the camera down, and those need different treatment. For that we run the listing reconcile from a home connection instead: it reads the city's real list, asks the server which of our rows are missing from it, checks each of those feeds is definitively gone rather than briefly unhappy, and retires only the cameras that fail both tests. Two conditions, because one API snapshot is not enough - ALGO Traffic in Alabama drops cameras and restores them between consecutive calls, and we have been caught by that before.
What is next
The same treatment now covers two more hosts that block us and serve readers fine: Autostrade per l'Italia, 1,036 cameras on the Italian motorway network, which ingested cleanly at 993 cameras on 2026-09-27; and NDAWN, the North Dakota agricultural weather network, 1,704 cameras of field and road. Both counted from our sources table at 13:06 UTC on 2026-09-29. Autostrade's cameras were all switched off on our side before we understood this, and they are back.
The bar for adding a host to that set is deliberately awkward, because getting it wrong means we blind-trust a source forever. A curl that returns 200 does not clear it. The host has to fail from our datacenter on the admin probe, succeed from a residential IP, and then actually render as a cross-origin <img> from the opencctv.org origin with referrerPolicy='same-origin', which is the step nothing on the command line can stand in for.
What I would like to get better at is finding these hosts before a reader does. We have a detector that groups recent socket timeouts by host and hides the ones already classified, and it is how NDAWN turned up, but it only fires after a source has been failing for a while. The earlier signal is sitting right there in the beacons: a host whose direct loads succeed for everyone whilst our own fetches time out is telling us exactly what it is. I have not written that query yet.