Library / SDK
fanout/django-eventstream avatar
fanout/django-eventstream

django-eventstream: pushing Server-Sent Events from a Django view, with the process boundary to respect

Server-Sent Events for Django

867 stars97 forksPythonMIT

At a glance

What is it?
django-eventstream adds SSE endpoints to a Django project, with channels, per-user permissions and optional event storage for recovery. The catch is architectural: without Redis or a GRIP proxy, send_event only works inside the server process.
Who is it for?
django-eventstream suits Django projects that want server to browser push over plain HTTP with no extra client library, especially where a single ASGI process or a GRIP proxy can carry the connections. It does not suit applications that need to publish from separate processes without adding Redis, or teams that require bidirectional messaging.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 92 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One never-ending HTTP response carries the events

django-eventstream provides Django views that push data to connected clients using the Server-Sent Events protocol. The mechanism is plain HTTP: a client opens a GET request and the response never ends, with new events appended as they occur.

A client asks for an endpoint like /events/ with the right Accept header:

http
GET /events/ HTTP/1.1
Host: example.com
Accept: text/event-stream

What comes back is a chunked response with a text/event-stream content type, where each event is a named type followed by a data line:

http
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Connection: Transfer-Encoding
Content-Type: text/event-stream

event: message
data: {"foo": "bar"}

event: message
data: {"bar": "baz"}

...

The appeal is that a browser consumes this without any client library, and native applications can do the same. The project lists easy consumption from browsers or native applications, reliable delivery through persisted events, per-user channel permissions, and compatibility with Django REST Framework as its features. The trade-off is that the connection is server to client only.

send_event takes a channel, an event type and the data

Publishing is one function call:

py
from django_eventstream import send_event

# send_event(<channel>, <event_type>, <event_data>)
send_event("test", "message", {"text": "hello world"})

The first argument names the channel, the second the event type, the third the payload. The payload is JSON encoded using DjangoJSONEncoder, so dictionaries and models with that encoder's handling become the wire format.

Channels are what the server side declares and the client side subscribes to. The URL configuration shows the pattern, with the channel list passed as extra arguments to the included URL set:

py
from django.urls import path, include
import django_eventstream

urlpatterns = [
    ...
    path("events/", include(django_eventstream.urls), {"channels": ["test"]}),
]

So a channel is declared at the URL level, and send_event later writes into one of the declared channels. Getting these two out of step is the most common way to find that an event goes nowhere.

The install itself is two packages, the module and the ASGI server it runs behind:

sh
pip install django-eventstream daphne

For Django REST Framework support there is an extra:

sh
pip install django-eventstream[drf] daphne

The project requires Python 3.10 or newer and Django 5 or newer, and the package metadata lists PyJWT, gripcontrol, django_grip and six among its dependencies, with djangorestframework as the optional drf extra.

The single process rule decides your architecture

This is the constraint to understand before anything else. In a basic setup, send_event must be called from within the server process, for example from a view. The README states plainly that it will not work if called from a separate process such as the shell or a management command.

The reason follows from how the library works. Without an external broker, an event has to reach the connection that is waiting for it, and that connection lives in the memory of the process holding it. A management command in a different process has no reference to it, so the event goes nowhere.

That single sentence decides the deployment shape. If you need events from cron jobs, from a Celery style worker, or from another service, you are out of the basic configuration. The README points to its scaling section for exactly that case, and it offers two routes: Redis, or a GRIP proxy.

The runserver command is for development. The README says explicitly that you should not use runserver when deploying, and to launch an ASGI server such as Daphne instead:

sh
daphne your_project.asgi:application

WSGI mode can work too, but only in combination with a GRIP proxy, which is the second route below.

Redis turns send_event into something any process can call

The Redis route is the simpler of the two answers to the process problem. The README lists two steps. First install the client:

sh
pip install redis

Then set the connection details in settings.py:

py
EVENTSTREAM_REDIS = {
    'host': 'redis',
    'port': 6379,
    'db': 0,
}

With that configuration in place, the README states that send_event will be able to send events from any process or instance of the application. That is the whole point: the broker holds the events, and the process holding a given connection picks them up.

The cost is an extra moving part. You now run Redis, and the application depends on it for event delivery. Whether that is a good trade depends on whether you already run Redis and whether you actually need to publish from outside the web process.

The same section makes the case for the other route. If you run multiple instances of your Django app for high availability or for scale, or you need to send events from management commands, the README says you can introduce Redis or a GRIP proxy, naming Pushpin and Fastly Fanout as examples of the latter.

Pushpin holds the long connections so Django can stay short lived

The GRIP route inverts the responsibility. A GRIP proxy such as Pushpin keeps the streaming connections open and relays requests to your application, so Django only handles short request and response cycles.

The README gives three configuration steps. Add the middleware and point GRIP_URL at the proxy's private control port:

py
MIDDLEWARE = [
    "django_grip.GripMiddleware",
    ...
]
py
GRIP_URL = 'http://localhost:5561'

The middleware comes from django-grip, which the README notes should already have been pulled in as a dependency of this module.

Next, route requests from the proxy to your app. Pushpin's routes file, usually at /etc/pushpin/routes, needs a line like this:

code
* localhost:8000

Finally, point your consuming clients at the Pushpin port, 7999 by default, since Pushpin forwards requests to your app and handles the streaming connections on its behalf. If a load balancer usually sits in front of the application, the README says it should be configured to forward to Pushpin instead, with an Nginx example:

code
location /api/ {
    proxy_pass http://localhost:7999
}

The trade-off here is different from Redis. You gain long lived connections without holding them in Django, and you lose the ability to publish from a separate process unless something else carries the event, so this route is about connection handling rather than about the process boundary.

Events are not stored by default, and clients will miss them

By default events are not persisted anywhere. The README is explicit about the consequence: if clients get disconnected, or if the server fails to send data, clients can miss messages. For reliable delivery you enable event storage, so that events can be persisted to your database and clients can recover after a disconnect.

The setup has two parts. Create the database tables with the usual migration command:

sh
python manage.py migrate

Then set a storage class in settings.py, following the project documentation for the exact key.

This is the feature that turns a nice demo into a system with delivery guarantees, and it is also the one with a cost: every persisted event occupies a row, and the storage class you choose determines how much history is kept and how clients catch up after a disconnection. The default of no storage is the right default for a demo, and a deliberate decision for anything else.

The DRF path depends on renderer order

Views can also be exposed through Django REST Framework. The README shows two ways to register them on the router, by wrapping a class with configure_events_view_set or by using EventsViewSet directly:

py
from django.urls import path, include
from django_eventstream.viewsets import EventsViewSet, configure_events_view_set

router = DefaultRouter()

# register by function
router.register(
    "events1",
    configure_events_view_set(channels=["channel1", "channel2"],
    messages_types=["message", "info"]),
    basename="events1")

# register by class
router.register(
    "events2",
    EventsViewSet(channels=["channel1", "channel2"]),
    basename="events2")

urlpatterns = [
    ...
    path("", include(router.urls)),
]

The renderers then decide how the stream is produced. DEFAULT_RENDERER_CLASSES in settings.py has to include the SSE renderer, and the browsable API variant is optional:

python
REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
        'django_eventstream.renderers.SSEEventRenderer',
        'django_eventstream.renderers.BrowsableAPIEventStreamRenderer'
         # Add other renderers as needed
    ]
}

The warning that comes with it is worth repeating: do not put the eventstream renderers before the JSONRenderer and BrowsableAPIRenderer, or the API will probably not work as expected. The order in that list is not cosmetic, and getting it wrong produces a failure that looks like a browser problem.

Where django-eventstream is the wrong tool

The first boundary is the protocol itself. Server-Sent Events are one way, from server to client. If the application needs the browser to send messages back over the same connection, or needs bidirectional messaging, this library does not provide it.

The second boundary is the deployment cost. The basic configuration works, but the moment you publish from anywhere other than a view, you are adding Redis or a GRIP proxy. For a small application with one process and events generated inside views, that is fine. For a service with background workers and scheduled tasks, the library is the reason you now operate a broker, and the trade has to be made deliberately.

The third boundary is the missing persistence. With no storage class configured, events are gone once sent, so a client that reconnects after a gap has no way to recover the messages it missed. That is acceptable for presence indicators and progress bars, and wrong for anything an audit trail depends on.

The fourth is version currency. The package metadata gives version 5.3.4 and classifies the project as Development Status 4 - Beta, and the repository has no GitHub releases to browse, so the version history lives in the package index and the source tree rather than in tagged releases. The last push to the repository was on 2026-07-23, and the repository is not archived. The project is MIT licensed, with the COPYING file at the root of the repository and the same licence declared in the package metadata.

Editorial conclusion

django-eventstream suits Django projects that want server to browser push over plain HTTP with no extra client library, especially where a single ASGI process or a GRIP proxy can carry the connections. It does not suit applications that need to publish from separate processes without adding Redis, or teams that require bidirectional messaging. Before adopting it, decide between the ASGI server, Redis and Pushpin routes, because the choice determines where send_event can be called from.

Frequently asked questions

How do I install django-eventstream?

Install the module together with an ASGI server, for example pip install django-eventstream daphne, or pip install django-eventstream[drf] daphne for Django REST Framework support. Then add daphne and django_eventstream to INSTALLED_APPS and set ASGI_APPLICATION in settings.py.

Why does send_event not work from a management command?

In a basic setup send_event only works when called from within the server process, such as from a view, because the event has to reach a connection held by that process. The README says that introducing Redis or a GRIP proxy such as Pushpin lets you send events from separate processes or instances.

How do I make django-eventstream deliver events reliably?

Enable event storage so events are persisted to your database. The README says to run python manage.py migrate and then set a storage class in settings.py, and that clients can then recover after a disconnection.

What does Pushpin do in a django-eventstream setup?

Pushpin is a GRIP proxy that holds the streaming connections and forwards requests to the application. Clients connect to the Pushpin port, 7999 by default, and the application talks to Pushpin through the django_grip middleware and the GRIP_URL setting.

Official sources

  1. fanout/django-eventstream on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/fanout-django-eventstream.svg)](https://hysenlabs.com/projects/fanout-django-eventstream)

Was this useful?