SSE under WSGI and ASGI#
Server-Sent Events carry patch envelopes to every open tab. The same envelope that answers an HTTP request rides the stream as an event. This page covers the stream helper, the refresh fan-out, the echo suppression, and the WSGI and ASGI contract that decides whether the stream can send a heartbeat.
The stream helper#
PatchEventStream is a StreamingHttpResponse returned from a page’s render escape hatch.
There is no dedicated SSE endpoint and no new public URL.
The page view authorises the subscriber, the same as any other page.
from collections.abc import Iterator
from django.http import HttpRequest
from polls.broker import broker
from polls.models import Poll
from polls.providers import DPoll
from next.partial import Patches, PatchEventStream
def patch_source(request: HttpRequest, poll_id: int) -> Iterator[Patches]:
"""Yield one refresh envelope for every poll change."""
for change in broker.changes(poll_id):
yield Patches(request, echo_of=change.request_id).refresh(zone="poll-results")
def render(request: HttpRequest, poll: DPoll[Poll]) -> PatchEventStream:
"""Open the patch event stream for one poll."""
return PatchEventStream(request, patch_source(request, poll.pk))
Each Patches the source yields becomes one next-patches event, serialised by the active protocol backend, the same shape an HTTP response carries.
A data-next-sse="/url/" element on the page opens the EventSource and routes each event into the same apply pipeline an HTTP response uses.
The refresh fan-out#
The recommended fan-out is the refresh verb.
The stream signals that a zone is stale, and every tab re-fetches the zone with its own cookies through the page view.
The stream carries no HTML.
event: next-patches
data: {"version":"9f3c2e1b","request_id":"1c9f…-r1","ops":[{"op":"refresh","zone":"poll-results"}]}
Authorization stays in the subscriber’s view, because the zone GET travels the page’s own URL, the same view, the same guards, the same middleware as a full load. The stream never broadcasts one user’s HTML to another, and the event channel does not have to know who may see what.
The fan-out is built on refresh rather than a context patch on purpose.
A context patch carries the value of a registered serialize provider, and the builder reads that value from the origin page of the request that creates it.
A stream source has no page-render origin, so it cannot build a context patch from one.
A stream that needs to push fresh context drives a refresh, and the re-fetched zone delivers the new context through its own render.
This is a documented limitation.
The stream source addresses zones to refresh, not provider values to push directly.
Echo suppression#
The tab that triggered the change already has the fresh zone from its own response and must not apply the fan-out again.
The application channel threads the mutation’s X-Next-Request-Id to the stream source, and the builder takes it as echo_of.
Patches(request, echo_of=change.request_id).refresh(zone="poll-results")
The serialiser stamps echo_of as the envelope’s request_id.
The client keeps a ring buffer of its recent X-Next-Request-Id values and drops an event whose id matches.
When the buffer overflows under a flood of submissions the degradation is safe.
The subscriber applies an extra refresh rather than failing.
The framework does not smuggle the request id through the broker. A change event has to carry it, which the broker does by recording the request id of the mutation that produced it.
WSGI and ASGI#
A stream holds a connection open, and how it stays alive when idle depends on the source and the server.
The source kind and the server are paired by contract.
An async patch source requires ASGI and a sync source requires WSGI.
A mismatch raises ImproperlyConfigured when the response is built.
Without the guard the wrong-kind iterator buffers in full and hangs the stream before the first byte.
Pair an async source with ASGI and a sync source with WSGI.
A sync source under WSGI sends no heartbeat.
A blocked next() on a sync iterator has nothing to interrupt it without a thread, so a quiet sync stream stays quiet.
A keepalive on a sync source is the source’s own responsibility, for example a comment frame on a wait timeout.
Under WSGI the stream also occupies one worker for the full life of the connection, so the recommendation is ASGI for any real workload.
An async source under ASGI receives heartbeat comments.
The stream interleaves a heartbeat during a quiet period through asyncio.wait(), so a buffering proxy keeps the connection.
The heartbeat period is SSE.HEARTBEAT_SECONDS in PARTIAL_BACKENDS.
Source and server |
Heartbeat |
Worker cost |
|---|---|---|
Sync source, WSGI |
None, keepalive is the source’s job |
One worker per open connection |
Async source, ASGI |
Sent on a quiet period |
One task per open connection |
To move a stream from sync to async, swap the broker’s wake primitive for an async one and pass an async source to PatchEventStream.
The page module and the signal layer do not change.
Stream politeness#
PatchEventStream sets the politeness headers on construction so a proxy or GZipMiddleware does not eat the flush.
Cache-Control: no-cache, no-transformkeeps a proxy from buffering and re-compressing the stream.X-Accel-Buffering: noturns off nginx buffering.The leading
retryhint comes fromSSE.RETRY_MSso the browser’s native reconnect uses the configured interval.
On the client a background tab pauses the stream by closing the connection.
When the tab becomes visible the runtime reconnects and re-fetches the zones the stream addressed since the connection opened.
A brief flicker between tabs reconnects the stream but skips the re-fetches, because only a tab hidden past a short threshold revalidates.
The set of tracked zones is bounded, so a long sleep cannot storm the server on resume.
Events missed while paused are not lost, because refresh is idempotent.
The re-fetch brings the current state regardless of how many fan-outs were missed.
See also#
See also
Partial rendering by scenario for the live stream scenario end to end.
Partial rendering reference for the SSE settings and the lifecycle events.
WSGI and ASGI for choosing a server.
Signals for the sse_stream_opened and sse_stream_closed signals.