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

Streaming Wire Formats: SSE and WebSockets
Photo by Logan Voss / Unsplash

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 opcode
  • 05 = payload length
  • 68 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.