Posts Quizzes AI Interviewer Feedback
Login Register

Python

Filtering by tag: Python Clear
TECH

SSE Event Streaming With Redis

Karen Feb 4, 2026

Event streaming is a way of processing data as a continuous flow of events rather than as one-time requests. Each event represents something that happened (e.g., “user signed up”, “order placed”), and systems can react to these events in real time or near real time. Event streaming enables systems to publish, consume, store, and process events continuously and asynchronously, making it well suited for real-time features such as notifications, dashboards, and live updates.

Pub/Sub Model

The pub/sub model is a messaging pattern where publishers send messages and subscribers (clients) receive them. Redis provides such mechanism using channels, where messages published to a channel are delivered to all active subscribers.

Messages in Redis pub/sub are not persisted, meaning that if a subscriber is offline, it will miss any messages published during that time. There is also no built-in replay support. Despite these limitations, Redis pub/sub is extremely efficient and well suited for real-time notifications, such as chat systems or live status updates.

Streaming API (View)

In this post we will implement a Django Server-Sent Events (SSE) endpoint that streams messages from a Redis pub/sub channel to connected clients in real-time.

The stream_events view subscribes to the demo_stream Redis channel and continuously listens for new messages. Whenever a message arrives, it is yielded as an SSE-formatted response using Django’s StreamingHttpResponse.

import redis
from django.http import StreamingHttpResponse
import time

def stream_events(request):
    r = redis.Redis(host="redis", port=6379, db=0)
    pubsub = r.pubsub()
    pubsub.subscribe("demo_stream")

    def event_stream():
        try:
            print("connected")
            while True:
                message = pubsub.get_message(timeout=1)
                if message and message["type"] == "message":
                    data = message["data"].decode("utf-8")
                    print(data)
                    yield f"data: {data}\n\n"
                time.sleep(0.01)
        except GeneratorExit:
            # Client disconnected
            pass
        finally:
            pubsub.close()
    response = StreamingHttpResponse(
        event_stream(), content_type="text/event-stream")
    response['Cache-Control'] = 'no-cache'
    return response

This implementation keeps the HTTP connection open and continuously pushes updates to the client whenever new messages are published to Redis.

Publishing Messages

Messages can be published to Redis using redis-cli or a Python Redis client, and any client visiting /stream/ will receive them live.

From the Redis CLI running inside Docker, messages can be published with 

docker exec -it redis redis-cli publish demo_stream "Test Event"

while from the Python Redis client, a sample example looks like this:

import redis, time

r = redis.Redis(host="127.0.0.1", port=6379, db=0)
for i in range(5):
   msg = f"Message #{i}"        
   r.publish("demo_stream", msg)
   print("Published:", msg)     
   time.sleep(1)

Each published message is immediately pushed to all connected SSE clients.

Accessing the Stream

The streaming url is 

path('stream/', stream_events, name="stream")

When visiting /stream/ in the browser (or via a compatible client), any published messages will appear in real time, as long as the client remains connected.

Note on Sync vs Async

This view runs in synchronous mode. If the Django application is running under an async server (like Daphne or Uvicorn), then a synchronous streaming view that uses blocking operations may block the event loop. This can cause the entire application to become unresponsive. To avoid this, make sure that either:

  • the whole application runs in synchronous mode, or

  • the view is rewritten to be fully async and non-blocking.

Read more
TECH

Sync and Async API Performance Comparison

Karen Jan 24, 2026

Asynchronous APIs are designed to handle many concurrent requests efficiently by avoiding thread blocking I/O operations (such as database queries, network calls, or file access). Instead of assigning one worker per request, async APIs use an event loop to switch between tasks, allowing better resource utilization under high concurrency.

In this article we will compare the performance of a sync django view running with a gunicorn server and an async django view running with an async uvicorn server. In both cases we call a test api and although it is expected for the async view running in a event loop and not being constrained by CPU threads to be significaltly faster, the results show the opposite.

To test the performance, we use hey load testing tool, which can be installed on Linux systems using:

sudo apt install hey

Defining Views

We define two views, sync_view and an async_view, both calling the same external API endpoint.

import requests
from django.http import JsonResponse
import httpx


def sync_view(request):
    r = requests.get("https://jsonplaceholder.typicode.com/todos/1")
    return JsonResponse(r.json())


async def async_view(request):
    async with httpx.AsyncClient() as client:
        r = await client.get("https://jsonplaceholder.typicode.com/todos/1")
    return JsonResponse(r.json())

The routes are:

/sync/test → sync view
/async/test → async view

and the corresponding URL mappings are:

path('async/test', async_view, name="async-test"),
path('sync/test', sync_view, name="sync-test"),

Load Testing Setup

The tests were run inside Docker containers using:

  • Gunicorn for the sync view
  • Uvicorn for the async view

Sync View Testing

To test the sync view performance (sync/test). We first run the Django server with Gunicorn with 4 workers and 10 threads per worker, This means simutanously 40 requests can be served.

gunicorn project.wsgi:application --workers 4 --threads 10 --bind 0.0.0.0:8000

We send 150 requests 30 of them being concurrent, 300 requests with 50 concurrency and 1000 requests with 100 concurency and log the results.

Async View Testing 

To test the async view performance (async/test), we run the Django server with uvicorn. 

uvicorn project.asgi:application --workers 4 --host 0.0.0.0 --port 8000

Terminology

  • Requests Per Second (RPS) measures the number of requests a server is able to handle and complete per second under a given load.

  • Average latency shows how long a request takes on average.

  • P95 latency means that 95% of requests completed faster than a given value (for example, 1.22s).

Results

Testing results are presented in the table bellow. 

Test Mode RPS Avg latency P95 latency
150 / 30 Sync 41.3 0.45s 1.22s
  Async 35.0 0.69s 1.52s
300 / 50 Sync 86.2 0.46s 0.93s
  Async 38.1 0.94s 1.69s
1000 / 100 Sync 81.4 0.92s 1.88s
  Async 42.0 1.75s 3.04s

The results show that sync view consistently outperforms the async view, as the the sync view has higher RPS, lower average latency and better tail latency.

Takeaway

Async view does not automatically imply faster performance, and for single outbound API calls at moderate load, sync views can often be faster, simpler, and more predictable. 

In this case, the sync setup performs well because Gunicorn provides enough threads to efficiently wait for I/O without overwhelming the system.

Read more