Streaming Wire Formats: SSE and WebSockets
Compare Server-Sent Events (SSE) and WebSockets at the wire level, and see how Uvicorn maps them into ASGI events. Keywords: Server-Sent Events, SSE, WebSocket, ASGI, Uvicorn, FastAPI internals
HTTP is flexible, it’s not just request-response. Two popular ways to push data to clients are Server-Sent Events (SSE) and WebSockets.
- SSE: lightweight, text-based streaming over plain HTTP.
- WebSocket: binary framing protocol that upgrades from HTTP.
In this post, we’ll look at the exact wire formats that reach Uvicorn, and how they appear as ASGI events.
Server-Sent Events (SSE)

SSE uses a regular GET request with a special header:
GET /events HTTP/1.1
Host: example.com
Accept: text/event-stream
The server responds with headers and then streams text:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
data: hello
data: world
Each event is separated by a blank line.
ASGI mapping
Uvicorn forwards SSE chunks as repeated http.response.body events:
await send({
"type": "http.response.start",
"status": 200,
"headers": [(b"content-type", b"text/event-stream")],
})
await send({
"type": "http.response.body",
"body": b"data: hello\n\n",
"more_body": True,
})
await send({
"type": "http.response.body",
"body": b"data: world\n\n",
"more_body": False,
})
Try it yourself with curl:
curl -N http://127.0.0.1:8000/events
(-N keeps the connection open.)
WebSockets

Unlike SSE, WebSockets require an upgrade handshake.
Upgrade request
GET /chat HTTP/1.1
Host: example.com
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13
Server handshake response
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
After this, the protocol switches to frames.
Example text frame
A small “hello” text frame at the byte level might look like:
81 05 68 65 6c 6c 6f
81= FIN + text frame opcode05= payload length68 65 6c 6c 6f= UTF-8 for “hello”
ASGI mapping
This frame appears as:
{"type": "websocket.receive", "text": "hello"}
And your app can reply:
await send({"type": "websocket.send", "text": "hi"})
Which Uvicorn frames and sends back to the client.
Conclusion
- SSE is simple: text over HTTP with repeated body chunks.
- WebSockets are more complex: handshake plus binary frames.
Both end up as familiar ASGI events in your app. Understanding these wire-level differences helps you pick the right tool: SSE for lightweight server pushes, WebSockets for full duplex communication.
In the final post, we’ll look at file uploads and range requests.