{"id":13719824,"url":"https://github.com/unbit/uwsgi-sse-offload","last_synced_at":"2025-05-12T18:27:52.006Z","repository":{"id":25208766,"uuid":"28632726","full_name":"unbit/uwsgi-sse-offload","owner":"unbit","description":"uWSGI offload bridge between redis pubsub and server sent events (sse)","archived":false,"fork":false,"pushed_at":"2015-01-01T09:03:30.000Z","size":172,"stargazers_count":22,"open_issues_count":0,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-20T15:44:25.440Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/unbit.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-12-30T13:58:59.000Z","updated_at":"2024-11-28T16:30:27.000Z","dependencies_parsed_at":"2022-07-14T00:40:29.170Z","dependency_job_id":null,"html_url":"https://github.com/unbit/uwsgi-sse-offload","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unbit%2Fuwsgi-sse-offload","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unbit%2Fuwsgi-sse-offload/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unbit%2Fuwsgi-sse-offload/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unbit%2Fuwsgi-sse-offload/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/unbit","download_url":"https://codeload.github.com/unbit/uwsgi-sse-offload/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253797216,"owners_count":21965845,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-03T01:00:56.092Z","updated_at":"2025-05-12T18:27:51.953Z","avatar_url":"https://github.com/unbit.png","language":"C","readme":"uwsgi-sse-offload\n=================\n\nuWSGI offload bridge between redis pubsub and server sent events (sse)\n\nThis is a fork of the uwsgi-realtime (https://github.com/unbit/uwsgi-realtime) project, exposing only sse features.\n\nThe bridge waits on a redis pubsub channel and whenever it receives a message it forwards it to the connected sse clients.\n\nIt is an offload engine so you can manage thousand of concurrent requests without bothering your workers/threads/async-cores\n\nHow it works\n============\n\nA client (read: a webbrowser) open an SSE connection to the webserver/proxy forwarding the request to uWSGI.\n\nuWSGI (or your app) recognize (via internal routing or via special response headers) it is an sse session and forward it to the offload engine. The offload engine subscribe to a redis pubsub channel and starts waiting for messages.\n\nWhenever a message is enqueued, the offload engine collects it and forward to the connected client.\n\nRemember: the offload engine is fully non-blocking so you can manage thousand of clients concurrently while your blocking main app continues its job. The maximum number of clients is defined by about half of the file descriptors limit\n\nInstallation\n============\n\nThe plugin is 2.0 friendly (it requires uWSGI \u003e=2.0.8):\n\n```sh\nuwsgi --build-plugin https://github.com/unbit/uwsgi-sse-offload\n```\n\nwill generate sse_offload_plugin.so in the current directory\n\nyou can eventually build a monolithic binary with sse-offload plugin in one-shot:\n\n```sh\ncurl http://uwsgi.it/install | UWSGI_EMBED_PLUGINS=\"sse_offload=https://github.com/unbit/uwsgi-sse-offload\" bash -s psgi /tmp/uwsgi\n```\n\nthis will result in a binary in /tmp/uwsgi with psgi and sse-offload support\n\nin the same way (for a python setup):\n\n```sh\nUWSGI_EMBED_PLUGINS=\"sse_offload=https://github.com/unbit/uwsgi-sse-offload\" pip install uwsgi\n```\n\nUsage (via internal routing)\n============================\n\nLet's start with a simple perl clock (so ensure your uWSGI instance has the perl/psgi plugin loaded or embedded). A perl script will publish in the 'clock' redis channel the current unix time (seconds since the epoch):\n\n```perl\nuse Redis;\n\nmy $redis = Redis-\u003enew;\n\nwhile(1) {\n        sleep(1);\n        $redis-\u003epublish('clock', time);\n}\n```\n\nsave it as clock.pl\n\nnow we want an html page that \"subscribe\" to the /whattimeisit url via sse (server sent events) and set the content of the 'clock' div to the received data (yes, they are the timestamps sent to redis)\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"utf-8\" /\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cscript\u003e\n    var source = new EventSource('/whattimeisit');\n    source.onmessage = function(e) {\n      document.getElementById('clock').innerHTML = e.data;\n    };\n  \u003c/script\u003e\n\n\u003cdiv id=\"clock\" /\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n(save it as clock.html)\n\nfinally we start uWSGI with a mule running clock.pl and a rule for mapping requests to /whattimeisit to the sse engine:\n\n```ini\n[uwsgi]\n; eventually use absolue path for the plugin if it is not in the current directory\nplugin = sse_offload\n; bind on http port 9090\nhttp-socket = :9090\n; run clock.pl as a mule\nmule = clock.pl\n; map requests to /clock to the html file\nstatic-map = /clock=clock.html\n; route requests to ^/whattimeisit to the sse engine subscribed to the 'clock' redis channel\nroute = ^/whattimeisit sse:clock\n; enable 1 offload thread\noffload-threads = 1\n```\n\nopen http://127.0.0.1:9090/clock (or whatever url the instance is bound) and (if all goes well) start seeing the unix time\n\nUsage (app-governed)\n====================\n\nIf you want to hold control over the sse url (for example for managing authentication and authorization) you can pass control of the sse url to your app and (after having done your checks) pass back the control to the offload engine.\n\nThere are various ways to accomplish this, the easiest is using uWSGI request vars (this time we use python, so ensure to load the python plugin too if not available in your binary):\n\n```python\nimport uwsgi\ndef application(environ, start_response):\n    if environ['PATH_INFO'] == '/whattimeisit':\n        uwsgi.add_var('X-SSE-OFFLOAD', 'clock')\n        return []\n    else:\n        start_response('200 OK', [('Content-Type', 'text/plain')])\n        return ['Hello World']\n```\n\n(save it as clock.py)\n\nso when the PATH_INFO is '/whattimeisit', your app set the X-SSE-OFFLOAD variable to the name of the channel to subscribe. Now let's configure uWSGI to honour this variable:\n\n```ini\n[uwsgi]\nplugin = python\n; eventually use absolue path for the plugin if it is not in the current directory\nplugin = sse_offload\n; bind on http port 9090\nhttp-socket = :9090\n; run clock.pl as a mule\nmule = clock.pl\n; map requests to /clock to the html file\nstatic-map = /clock=clock.html\n\n; load the wsgi app\nwsgi-file = clock.py\n\n; tell the routing engine to check for X-SSE-OFFLOAD variable\nfinal-route-if-not = empty:${X-SSE-OFFLOAD} sse:${X-SSE-OFFLOAD}\n; enable 1 offload thread\noffload-threads = 1\n```\n\nthe 'final-route-if-not' rule tells the engine to run the 'sse' action if the X-SSE-OFFLOAD variable is not empty, passing its content as the sse action argument.\n\nThe sse engine is 'smart' about response headers, so you are free to generate them from your app without damaging the stream:\n\n```python\nimport uwsgi\ndef application(environ, start_response):\n    if environ['PATH_INFO'] == '/whattimeisit':\n        uwsgi.add_var('X-SSE-OFFLOAD', 'clock')\n        start_response('200 OK', [('Content-Type', 'event/stream'), ('Cache-Control', 'no-cache'), ('Foo', 'Bar')])\n        return []\n    else:\n        start_response('200 OK', [('Content-Type', 'text/plain')])\n        return ['Hello World']\n```\n\nor with Django:\n\n```python\ndef sse_view(request, foobar):\n    response = HttpResponse('', content_type='event/stream')\n    response['Cache-Control'] = 'no-cache'\n    uwsgi.add_var('X-SSE-OFFLOAD', 'clock')\n    return response\n```\n\nUsing --collect-header and --pull-header\n========================================\n\n--collect-header is a uWSGI option for mapping a response header to a request variable automatically:\n\n```ini\n[uwsgi]\n; this will place the value of Content-Type in RESPONSE_TYPE variable\ncollect-header = Content-Type RESPONSE_TYPE\n...\n```\n\nIn this way we can avoid the use of uwsgi.add_var() api function and automatically detect SSE responses to offload:\n\n```ini\n[uwsgi]\n; this will place the value of Content-Type in RESPONSE_TYPE variable\ncollect-header = Content-Type RESPONSE_TYPE\n; route to sse offload engine if RESPONSE_TYPE is event/stream\nfinal-route-if = equal:${RESPONSE_TYPE};event/stream sse:channel\n...\n```\n\nYou can eventually pass to name of the channel via response headers too (again a Django example):\n\n```python\ndef sse_view(request, foobar):\n    response = HttpResponse('', content_type='event/stream')\n    response['Cache-Control'] = 'no-cache'\n    response['X-SSE-Channel'] = 'foobar'\n    return response\n```\n\n```ini\n[uwsgi]\n; this will place the value of Content-Type in RESPONSE_TYPE variable\ncollect-header = Content-Type RESPONSE_TYPE\ncollect-header = X-SSE-Channel X_SSE_CHANNEL\n; route to sse offload engine if RESPONSE_TYPE is event/stream\nfinal-route-if = equal:${RESPONSE_TYPE};event/stream sse:${X_SSE_CHANNEL}\n...\n```\n\nthis will work but the X-SSE-Channel response headers will be sent to the client too and you could not want it.\n\nFor solving it, you can use the --pull-header option, that works like --collect-header but do not send the specific header to the client (read: it only maps it to a request variable)\n\nNote: --pull-header has been in added in uWSGI 2.0.9\n\n```ini\n[uwsgi]\n; this will place the value of Content-Type in RESPONSE_TYPE variable\ncollect-header = Content-Type RESPONSE_TYPE\npull-header = X-SSE-Channel X_SSE_CHANNEL\n; route to sse offload engine if RESPONSE_TYPE is event/stream\nfinal-route-if = equal:${RESPONSE_TYPE};event/stream sse:${X_SSE_CHANNEL}\n...\n```\n\n\nAction parameters\n=================\n\nThe 'sse' routing action takes a single parameter (the redis channel) or a keyval list:\n\n```ini\nroute = ^/foobar sse:server=127.0.0.1:4040,subscribe=foobar\n```\n\nthis will connect to the redis server 127.0.0.1:4040 subscribing to the channel 'foobar'\n\nThe folowing keys are available:\n\n* `server` (the redis server address, unix sockets are supported too)\n* `subscribe` (the channel to subscribe to)\n* `buffer_size` (the buffer size for the response, default 4k, tune it only if you need to stream big messages for which having a bigger buffer could result in better performance)\n\n\nThe 'raw' mode\n==============\n\nThe 'sse' action, takes every message from the redis channel and 'convert' it to sse format:\n\n```\nfoobar\n```\n\nbecome\n\n```\ndata: foobar\\n\\n\n```\n\nthe 'converter' take rid of multiline messages too:\n\n```\nfoobar\\n\nfoobar2\n```\n\nis converted to\n\n```\ndata: foobar\\n\ndata: foobar2\\n\\n\n```\n\nIf you want to disable this convertion and directly stream out the content of the redis message as-is, use the sseraw action:\n\n```ini\nroute = ^/foobar sseraw:server=127.0.0.1:4040,subscribe=foobar\n```\n\nTips\u0026Tricks\n===========\n\nRemember your app can publish messages to redis too, so you can implement realtime notifications pretty easily.\n\nSome example:\n\n* add a signal to a Django \"News\" model that publish to redis every time a new item is added (so connected peers will be notified of latest news in real time)\n* albeit sse can only receive data, you can make ajax requests in your html page triggering a redis publish. In this way you have an almost full-duplex communication (sync for posting, async for receiving). Building a chat with this approach will be really easy (and cheap)\n* do not limit yourself to a single channel, use multiple redis channels for multiple purposes\n","funding_links":[],"categories":["C"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Funbit%2Fuwsgi-sse-offload","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Funbit%2Fuwsgi-sse-offload","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Funbit%2Fuwsgi-sse-offload/lists"}