Sitemap

Real-Time Notifications in Python: Using SSE with FastAPI

9 min readJun 19, 2025

--

Press enter or click to view image in full size

Modern web applications increasingly rely on real-time updates, such as live chat, dashboards, and notifications. Users expect instantaneous feedback (such as new messages, alerts, and data streams) without having to refresh manually. Traditional request/response isn’t sufficient for this, so “push” technologies are crucial. Server-Sent Events (SSE) offer one such solution, enabling the server to push updates to the client over HTTP, thereby eliminating the need for the client to poll.

✨ See full project on GitHub

What Are Server-Sent Events (SSE)?

Server-Sent Events (SSE) is a browser-native API (part of HTML5) for unidirectional server→client streaming. In SSE, the client opens a persistent HTTP connection (with Accept: text/event-stream), and the server continues to send text messages over that channel. Unlike WebSockets, the channel is one-way: the server pushes data, and the client listens. The EventSource API in JavaScript handles the connection and parses incoming events. Each message is sent as plain text, e.g., data: …\n\n, and the browser dispatches it as an event. SSE also supports optional fields (e.g., event:, id:) to label events or resume streams.

For example:

event: ping
id: 123
data: {"time": "2025-06-12T11:30:00Z"}

SSE vs WebSockets vs Polling: What Should You Use?

  • Long-Polling: The client repeatedly sends requests asking for new data. It’s inefficient but straightforward: most requests return no new data. It adds latency (each poll waits) and loads the server with constant new connections.
  • SSE: Ideal for one-way streaming from the server to many clients. It’s easy to implement (just set Content-Type: text/event-stream and stream chunks), works over HTTP/1.1 or HTTP/2, and auto-reconnects if dropped. SSE saves bandwidth by using a single long-lived connection rather than making frequent polls. However, it can’t send messages from the client to the server.
  • WebSockets provide full-duplex, bidirectional communication over a single TCP connection. It’s best for chat apps, multiplayer games, or any scenario where both the client and server need to send messages. However, WebSockets require more setup and specialized server support, and may not traverse proxies as easily. They also maintain more overhead per connection (though still efficient). In practice, choose SSE for simple server→client updates (notifications, live feeds) and WebSockets when you need client→server streaming or lower latency two-way communication.

SSE is ideal for low-frequency, one-way updates like notifications or status changes. It’s lightweight and easy to implement. For high-frequency or bidirectional communication, WebSockets provide better scalability and lower latency under load.

How Server-Sent Events Work Under the Hood

Underneath, SSE is just chunked HTTP streaming. The client makes a standard HTTP GET with Accept: text/event-stream, and the server responds with headers like:

HTTP/1.1 200 OK  
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

Then keeps the connection open indefinitely. Each event is sent as UTF-8 text. Single newlines separate fields in an event (such as data or event), and a blank line (\n\n) ends each event. For example:

data: {"price": 123.45}
data: USD
id: 42

Here \n\n after the id: 42 signals the event is complete. The TCP connection remains open (no final 0-length chunk is sent), and the server can later write more chunks as new events occur. Because HTTP/1.1 supports persistent connections and chunked transfer encoding, the server doesn’t need to know the content length in advance. SSE runs entirely over standard HTTP ports (80/443), allowing it to pass through firewalls without special configuration.

The browser uses the EventSource interface to parse these chunks into message events. It also handles automatic reconnection: if the connection drops, the browser retries by default, even sending the Last-Event-ID header if the last message had an ID. This lets the server resume the stream without duplicating messages. In short, SSE cleverly transforms HTTP’s request-response model into a continuous one-way data stream.

SSE in Python: The Benefits and the Challenges

In Python, SSE’s HTTP-based nature is both a benefit and a challenge. On the plus side, you can use any web framework to stream text responses and set the Content-Type header to text/event-stream. No special protocol (such as WebSocket) is required, and existing HTTP servers (including ASGI frameworks like FastAPI or even WSGI with some adjustments) can handle it. SSE is lightweight, stateless, and typically easier to test than WebSockets. Libraries like sse-starlette provide helpers, and Python’s async features (asyncio, FastAPI/Starlette) make streaming straightforward.

On the downside, SSE is unidirectional (server→client only), so it can’t replace cases where the client must push data (like chat input). Python’s typical WSGI servers (e.g., Gunicorn in sync mode) may not stream well, so you often need ASGI servers (Uvicorn, Daphne) or disable buffering. Additionally, maintaining multiple HTTP connections open consumes resources (although SSE is more efficient than polling, each open connection still utilizes a socket and memory). Concurrency must be managed; for example, in a multi-process server, one process may not see updates from another unless shared storage (such as Redis or a database) is used. The client-side reconnection and “Last-Event-ID” logic must also be handled: if events have an “id” field, the browser will automatically include it on reconnect. Overall, SSE in Python works best when you structure your app asynchronously or dedicate a worker to the SSE stream.

When to Use SSE Instead of WebSockets

Use SSE when your application primarily needs server→client push of events, and the client doesn’t need to send frequent data back. Typical scenarios include live notifications, news feeds, real-time dashboards, or any situation where the server initiates updates. SSE is simpler to implement in these cases (no handshake, uses HTTP) and integrates easily with HTTP caching and authentication. For instance, a notifications service where the server tells the client, “You have a new alert,” is a perfect SSE use-case.

In contrast, if you need full bi-directional streaming (where the client sends data back, such as in chat or gaming) or extremely low latency, WebSockets may be a better option. In summary, SSE excels for “one-way” real-time streams, while WebSockets are better suited for two-way or high-throughput needs.

Use Case: Live Notifications

Building a Real-Time Notification System in FastAPI

A typical SSE use case is a push notification system. For example, when a significant event occurs on the backend (such as a new message, system alert, or status change), you want all connected clients to be notified immediately. One approach is to have clients connect to an SSE endpoint (e.g., /notifications/) and have backend processes publish events to that endpoint’s stream.

Get İnan DELİBAŞ’s stories in your inbox

Join Medium for free to get updates from this writer.

In FastAPI, you might write an endpoint like /subscribe/{user_id} that keeps a stream open for that user. Meanwhile, whenever something happens (e.g., a background task completes or a database record updates), you publish an event to a shared broker (such as Redis). The SSE endpoint’s async generator listens on that broker and yields incoming messages. A simplified sketch:

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import aioredis, asyncio, json

app = FastAPI()
redis = await aioredis.from_url("redis://localhost")

async def notification_stream(user_id: str):
pubsub = redis.pubsub()
await pubsub.subscribe("notifications")
while True:
msg = await pubsub.get_message(ignore_subscribe_messages=True)
if msg and msg["type"] == "message":
event = json.loads(msg["data"])
# filter by user_id if needed...
yield f"data: {json.dumps(event)}\n\n"
await asyncio.sleep(0.01)

@app.get("/notifications/{user_id}")
async def notifications(user_id: str):
return StreamingResponse(notification_stream(user_id), media_type="text/event-stream")

On the backend, any part of the app (or a Celery task) can publish to Redis:

await redis.publish("notifications", json.dumps({"user": user_id, "text": "New alert!"}))

Then all clients subscribed via SSE will immediately receive that JSON. This pub/sub pattern is familiar; one guide notes using Redis pub/sub for precisely this purpose. You can also push events via other brokers (RabbitMQ, Kafka) or use a task queue (Celery) that, upon completion, calls redis.publish().

💻 You can find a complete working example of this structure on GitHub:

👉 https://github.com/inanpy/fastapi-sse-example

SSE Performance Tips for High-Traffic Apps

  • Compression: SSE streams are text-based, so you can (and should) enable gzip/deflate compression on your HTTP response. This can cut bandwidth by 70–90% for typical JSON payloads. Most web servers (e.g., nginx, uvicorn) support HTTP compression; ensure that the Content-Type: text/event-stream is compressible according to your configuration.
  • Minimal Data: Keep SSE messages small. Only send the data the client needs. Since each event uses one HTTP packet over TCP, minimizing payload improves throughput.
  • Batching Events: If events occur rapidly, consider batching multiple data items into a single SSE event (as a JSON array) rather than sending many back-to-back events. This reduces overhead (each event has some framing bytes).
  • Disable Buffering: Many reverse proxies (nginx, UWSGI) buffer responses by default, which defeats streaming. Disable buffering for SSE endpoints. For example, send the header X-Accel-Buffering: no (nginx) or configure proxy_buffering off. This ensures each yield is flushed to the client immediately.
  • Close Idle Streams: Always close SSE connections on the client side when they are no longer in use (to free server resources). On the server side, if you detect the client disconnect (e.g., GeneratorExit in Python), stop sending—unused open connections waste memory and file descriptors. Many examples explicitly check for broken clients and break the loop, or rely on async frameworks to cancel the generator.
  • Connection Limits (HTTP/1.1 vs HTTP/2): In HTTP/1.1, browsers typically allow only ~6 concurrent connections per origin. If your app must serve many SSE clients, use HTTP/2. With HTTP/2, a single TCP connection can multiplex dozens or hundreds of streams. Deploying your app on a server (or proxy) that supports HTTP/2 will greatly increase scalability.

Handling Multiple Clients and Scaling with ASGI Servers

To serve multiple concurrent SSE clients, use an ASGI server such as Uvicorn. This can handle asynchronous I/O efficiently. You can run multiple worker processes (or threads), but remember that each SSE stream lives in one process. If your application uses multiple processes (e.g., via Gunicorn workers), ensure that the event source (Redis or whatever) is shared; otherwise, one process won’t see events published in another. A good approach is to maintain a single central Pub/Sub (e.g., Redis, Kafka) that all processes subscribe to. Then, each instance streams to its clients independently.

If you need to broadcast to all clients, consider a pub/sub or message queue: publish an event once, have every server instance send it to its connected clients. Libraries like sse-starlette help manage multiple channels. Also, ensure you test your deployment. For high-scale applications, you may need load balancers or sticky sessions if clients are pinned to particular servers, or use global pub/sub so that any instance can serve any client.

What to Watch Out for in Production (Timeouts, Keep-Alive, CORS)

  • Proxy Timeouts: Be aware of reverse proxy settings. Many proxies (nginx, AWS ELB, etc.) will terminate idle streams after a timeout (often 60 seconds). To prevent this, either send periodic comment heartbeats (e.g., “: heartbeat\n\n”) or adjust the proxy’s keepalive/timeout settings. For nginx, you can set proxy_read_timeout to a high value or use X-Accel-Buffering: no as above.
  • CORS and Authentication: If your SSE endpoint is on a different origin or requires auth, configure CORS and credentials properly. In JS, you can create a new EventSource(url, { withCredentials: true }) to send cookies. On the server, set allow_origins and allow_credentials if using middleware (e.g., FastAPI’s CORSMiddleware). Ensure TLS (HTTPS) is used if credentials are involved.
  • Connection Limits: As noted, browsers limit the number of concurrent connections per domain. Avoid using too many SSE connections on a single page; instead, consider splitting them across subdomains or using HTTP/2. Test in all target browsers.
  • DNS/IP Blocking: Some CDNs or firewalls may treat long-lived HTTP connections unusually. Ensure your hosting environment supports streaming responses (some servers that buffer or require Content-Length may not work).
  • Resource Usage: Each SSE client uses one server thread or coroutine. For a large-scale system, monitor the open connection count and memory usage. It may be necessary to run on servers optimized for I/O (async) and to increase limits on file descriptors.

In Conclusion;

SSE remains a simple and effective tool for real-time notifications. It’s built on standard HTTP, widely supported, and ideal for unidirectional data streams. For many use cases, such as live alerts, feeds, and dashboards, SSE provides a lightweight alternative to more complex WebSocket setups. The trade-offs are now well understood: SSE excels at server→client updates with automatic reconnection, but isn’t bidirectional. When you need a straightforward push mechanism (and especially when you can leverage Python’s async frameworks), SSE is still a very relevant choice.

We have mature tooling: FastAPI and libraries like sse-starlette make building SSE streams easy. And for developers, SSE means handling real-time without leaving the comfort of HTTP. In short, SSE’s one-way streaming model remains useful, bridging classic web protocols and modern real-time user experiences.

Final Thoughts

Real-time capabilities are no longer a luxury — they’re a core expectation in modern web experiences. Whether you’re building a live dashboard, sending instant alerts, or updating a user’s feed, the ability to push updates seamlessly can transform how users interact with your application.

Server-Sent Events (SSE) offers a robust, elegant, and underrated approach to real-time communication. It’s easy to implement with FastAPI, integrates smoothly into existing HTTP infrastructures, and shines in scenarios where the data flow is one-way from server to client.

If your use case doesn’t demand bidirectional communication, SSE should be your default starting point, before jumping into the complexity of WebSockets.

Have you implemented real-time features using SSE in your projects?

I’d love to hear your thoughts, lessons, or questions in the comments.

If this article helped you, consider sharing it with your developer network or giving it a few claps 👏

Thanks for reading!

Inan Delibas
Python Team Lead & Architect
Linkedin | Github

--

--

İnan DELİBAŞ
İnan DELİBAŞ

Senior Backend Engineer | Python | Distributed Systems & Microservices | Writing about backend engineering and real-world technical challenges