One Answer, Delivered to Everyone
Open the tile grid and watch the Network tab for ten seconds. Five requests go out: one for the 50 cameras on screen, one for the camera you have open, and one per active filter. Ten seconds later, the same five again. Every tab in every county does the same thing, and every one of those requests makes Frontend.API rebuild the same answer from scratch — a Cosmos query, then fifty one-at-a-time Table Storage reads to find each camera's latest photo. Ten operators is fine. Ten thousand people during a fire is not, because the origin's work is a multiple of the audience, and nothing about the answer differs between them. Only who asked.
The Camera Snapshot stream is the fix. It's a way of computing that answer once, handing it out from the front door instead of the back office, and telling the people who are watching a camera when it changes — behind a single switch that puts everything back the way it was. This page explains how it works and how to operate it.
Built on feature/camera-snapshot-stream in the Alert.CA.ImageAcquisition mono repo,
Alert.CA.Frontend and Alert.CA.Terraform (PRs 3973, 3971, 3972). Ships with the flag off;
nothing on this page changes behaviour until the flag row exists.
Feel the difference first
Drag the audience up. On the left, browser tabs. In the middle, the Front Door edge. On the right, the origin: Frontend.API, Cosmos and Table Storage. Then switch designs.
Every tab reaches the origin. Origin and storage load grow with the audience.
What you just felt is the whole argument. With polling, every dot reaches the origin, and past a few tens of thousands of tabs it saturates — those are prd's real numbers, 30 instances at the load-tested 1.3k requests per second each. With the snapshot, the dots stop at the edge and the origin sees about twenty requests a second no matter where you drag the slider. The one line that still grows with the audience is edge hits, and absorbing that is the one thing a CDN exists to do.
Three ideas, in the order they were needed
Think of the app as a newsroom that needs to know what 1,200 cameras are doing right now.
Print the sheet once. Instead of answering each runner, the archive keeps one printed sheet — the snapshot — with every camera's current state, reprinted every five seconds. It doesn't re-read the whole archive to do that: every change to a camera record lands in Cosmos's change feed, and the reprint reads only what changed since the last one. The sheet is deliberately the same for everybody; anything that depends on who you are is left off it.
Hand out copies at the front desk. The sheet goes to Front Door, which keeps a copy for five seconds and hands it to anyone who asks — signed-in operators included, because the body is identical for them too. Ten thousand people means ten thousand copies from the desk and at most one trip to the archive every five seconds per desk. The private items an operator is entitled to (AI detection details, a few restricted cameras) come from a small separate note that is never left at the desk.
Send a note to whoever is watching. When a camera takes a photo, the capture writes the photo's location onto that camera's record; the change lands in the same feed; a small function reads it and sends "camera X changed" through the SignalR channel the app already has. Only the operators whose screens show camera X receive it, and their tile updates within about a second. They re-read the full sheet only every five minutes, as a safety net.
Walk through it
Press Next to follow one capture: first the way every tab used to ask for it, then the way one copy reaches everyone, then the way a change reaches the operator looking at it.
Click any node in the diagram for the real class, table, or entity behind that box.
Three flows, because three different things happen to one capture: the poll it used to trigger, the copy that reaches everyone, and the note that reaches the operators watching. Click a node for the file it lives in.
What an operator's browser actually does
useCameraSnapshotMode() is the one decision point. It returns snapshot only when the flag
is on and no degrade window is open; otherwise every existing poller runs exactly as it
did before. Nothing in the app checks the flag anywhere else, which is what makes the two
paths impossible to run at once.
In snapshot mode the root page loads the sheet, merges it into realTimeCameraStore — the
same store every tile, badge and the AI alert sound already read — and, for a signed-in user
with a live socket, subscribes to deltas for the tiles inside the viewport (capped at 60), the
selected camera and the open map callout's camera. Deltas are applied newest-wins in 300 ms
batches, so a busy grid re-renders about three times a second, below its old once-per-poll
rate. Anonymous visitors never subscribe; they re-read the edge copy every 10 seconds, the
same freshness they had, at no cost to the origin.
The three grid filters stop being requests. Offline comes from the sheet's offline list, the
same durable-entity table the old endpoint read. Active is every row whose movexp is in the
future, the old endpoint's own predicate. AI-detected compares each overlay hit's confidence
with your slider in the browser — move the slider and the set changes instantly, with nothing
on the wire.
The public snapshot masks the four AI booleans to false for everyone. If those masked values
reached the store, every resync would flip a detected camera to false and the private
overlay would flip it back — and useAiDetectionSound treats exactly that false → true
transition as a brand-new detection. So snapshot rows are merged without those four keys
(withoutAiFlags); only the overlay writes them.
The alert sound watches the store from a background tab through a direct subscription, so a firefighter with the app minimised still gets the chime. The snapshot and overlay queries therefore keep refetching in hidden tabs, as the legacy poll always did. A "sensible" default of pausing background refetches is a regression here.
The one switch
A single row in the SQL FeatureFlag table, enable-camera-snapshot-stream, is read by all
three tiers: Frontend.API's builder loop and ImageAcquisition's publisher re-read it every
30 seconds, the frontend reads it on page load. A missing row is off. Flip it, then press
the button to advance time and watch each tier notice.
Nothing else is needed to roll back: no deploy, no restart, no data migration.
That drill is the entire rollback. With the row at 0 the builder idles and the endpoint
answers 503 with Cache-Control: no-store, which Front Door never caches; every open tab
hits that 503 on its next fetch and runs the legacy pollers for five minutes before checking
again. The legacy endpoints, hub groups and handlers were never modified, so the old
behaviour is the untouched code, not a reconstruction of it.
Going deeper
Wrapping up
The mental model is a printed sheet, a front desk and a note: compute the fleet's state once from what changed, let the edge hand out copies to everyone, and push a change only to the operators looking at that camera. One flag row turns all of it on or off, and off means the untouched legacy code. Next, see the channel those notes travel on: One Socket, Three Destinations.