Place2Page
Why We Used SSE First and Polling as a Fallback

Engineering

Why We Used SSE First and Polling as a Fallback

Place2Page streams generation progress over authenticated SSE, then falls back to polling only when the live channel fails.

The product requirement was simple.

When someone pastes a place URL into Place2Page, the generation screen should feel alive. It should show what is happening now, not make the user refresh and guess.

The implementation choice was less simple.

We wanted live progress updates, but we also wanted a transport that worked with authenticated requests, backend job processing, and real networks that do not always behave perfectly.

Why SSE matched the job

The generation screen is mostly one-way communication.

The browser does not need a full duplex socket. It needs a steady stream of status updates from the server:

  • fetching place data
  • writing the brief
  • streaming HTML chunks
  • reaching complete or error

That is a good match for Server-Sent Events.

SSE keeps the client contract small. It is HTTP-based, easy to reason about, and well suited to "tell me what changed next" flows.

Why the main client path used fetch, not EventSource

Browser EventSource is convenient, but our stream endpoint is authenticated.

That mattered because the generation progress route needs the same access checks as the rest of the project APIs. In our case, the simpler product rule was "send the bearer token on the stream request too."

That pushed the main frontend path toward a fetch-based SSE parser. The browser still receives standard data: events, but the request is made through fetch, so the app can attach the Authorization header and keep the auth model consistent.

That choice sounds small, but it kept the responsibility boundary cleaner:

  • frontend owns the authenticated stream connection and event parsing
  • backend owns access checks, event delivery, and terminal events

Why polling stayed in the product

A lot of real systems stop at "we added streaming."

We did not want the interface to depend on that optimistic assumption.

Live connections can fail for reasons that have nothing to do with the generation job itself:

  • mobile networks changing underneath the request
  • browser or proxy buffering quirks
  • temporary stream disconnects
  • environments where a long-lived response is less reliable than ordinary HTTP polling

If the stream breaks and the UI has no fallback, the user sees a stuck screen even while the job is still making progress.

That is the worst version of the experience. The backend may be healthy, but the product feels broken.

So Place2Page treats polling as a deliberate fallback path, not as an embarrassing leftover.

The happy path is straightforward:

  1. Try authenticated SSE first.
  2. Update the running page from incoming events.
  3. If that connection fails, poll project state until generation finishes.

That gave us a more honest contract with the user. We prefer the live channel, but we do not force the whole experience to depend on it.

The event contract mattered as much as the transport

One lesson here is that "SSE versus polling" is only part of the design.

The more important question is what the frontend can safely rely on.

In our case, the useful contract was:

  • the backend emits ordered JSON events
  • done and error are explicit terminal events
  • the frontend can update status, previews, and chunked output incrementally
  • the project detail can be refreshed after completion

That is what makes the fallback tolerable. The product is not guessing whether generation is over. It is reading a small, explicit state machine.

What we chose not to overbuild

We did not jump to WebSockets. We did not make the browser speak a custom realtime protocol. We did not treat low-latency bidirectional communication as a requirement when the job itself is fundamentally server-led.

That restraint mattered.

The product did not need "maximum realtime." It needed "clear progress, reliable enough streaming, and a sane fallback when reality gets messy."

For Place2Page, that combination was better than chasing the fanciest transport.

Closing

The most useful part of this design is not that SSE is elegant.

It is that the product keeps working when the elegant path is unavailable.

SSE gives the generation screen the feeling of live progress. Polling makes sure the experience still resolves when the network does not cooperate.

That is usually a better trade-off than pretending one transport will always win.

Sources