Web Slinging

This is a short technical note describing how to setup aiohttp-wsgi and aiohttp to serve Django via WSGI while allowing WebSockets on the same port.

Hopefully this setup is useful to you for doing awesome things. Note, this is just a technical note to use as a starting point and I have not benchmarked the performance in any way. I work daily with (more or less) embedded systems, big and small, and all this web stuff is just a hobby, so to speak.

What you need

Firstly, mise en place. You need:

  • Python 3
  • aiohttp
  • aiohttp-wsgi
  • your Django app
  • nginx

Why do we use these

Python 3.4+ is a must since you need the asyncio support. This gives you asynchronous I/O, event loops, coroutines, and tasks. These basically let you do co-operatively scheduled single-threaded concurrent code which is quite useful when dealing with inputs/output handling from, say, the network.

The aiohttp is an (almost) pure Python HTTP server (and client), using the asyncio support. It provides WebSocket handling, among other things.

The aiohttp-wsgi is a WSGI adapter for aiohttp. This is a bit more flexible than the built-in aiohttp.wsgi. The lack of setup instructions in aiohttp-wsgi was actually the main motivation in putting up this note.

Your Django app, this is any Django application you want to make asyncio-capable, to add WebSocket support to, etc.

Nginx is a fast HTTP and reverse proxy server. You could probably also use HAProxy or similar tools as well.

Prepare aiohttp-wsgi

Note: be sure to use version 0.1.1 or higher, as in 0.1.0 Django responses will make aiohttp-wsgi nag with a RuntimeError.

The big picture

The satellite level view is as follows:

  • The client connects to port 8000.
  • Nginx auto-redirects HTTP (port 8000) to HTTPS (port 8443).
  • SSL handshake and setup are done, further traffic to and from 8443 is SSL protected.
  • Traffic coming to 8443 is decrypted internally and routed to aiohttp WSGI front-end at
  • Here the traffic goes either to WSGI app (Django) or WebSockets.
  • Response is sent to the client.

The ports are high ports to allow an unpriviledged user to test this - no need to be root. You can easily map the ports to suitable priviledged ones like 80, 443 and so on.

The directory structure

The directory tree is set up as follows:

├── httpd.py
├── nginx
│   ├── log
│   ├── nginx.conf
│   └── ssl
└── yourdjangoapp
    ├── manage.py
    └── yourdjangoapp
        ├── templates
        ├── urls.py
        ├── views.py
        └── etc.

Note: it is assumed that aiohttp and aiohttp_wsgi are installed system-wide. It's an unimportant detail - you can install into virtualenv instead (I did).

Your Django app

The Django app, which is called "yourdjangoapp" from now on, is a regular Django app. In this note, the app is stupendously simple and basically routes "/" to the index-method, which serves up a templates/index.html file.

The urls.py is setup like this:

from yourdjangoapp import views

urlpatterns = patterns('',
    url(r'^$', 'yourdjangoapp.views.index', name='index'),

...and a views.py is setup like this:

from django.shortcuts import render_to_response

def index(request):
    return render_to_response("templates/index.html")

Thus when "/" is accessed, the templates/index.html is sent to the client. The templates/index.html is a truly remarkable gem of state-of-the-art web design and contains:

<body bgcolor=white text=black>
<h1>This is the index...</h1>
<p>This site uses cookies. Get over it.</p>
    var socket = new WebSocket('wss://');
    socket.onopen = function(event) {
        console.log('a user connected');
        socket.send("Howdy ho");
    socket.onclose = function(event) {
        console.log('a user disconnected');
    socket.onerror = function(event) {
        console.log('a user got sour');
    socket.onmessage = function(event) {
        console.log('Server said: ' + event.data);
        // delayed reply
        setTimeout(function() { console.log('I said: Say it again'); socket.send("Say it again"); }, 2000);

The client will ping-pong messages with the server.

Notice the address wss:// This is the IP address of the Nginx reverse proxy SSL port with the WebSocket URL (see below) with an unpriviledged port used for SSL.

Note: you must add the following to yourdjangoapp/settings.py to use the index.html template.

TEMPLATE_DIRS = [("%s/yourdjangoapp" % BASE_DIR)]

The aiohttp WSGI front end (httpd.py)

The httpd.py is a front-end component to manage routing to WebSockets and to Django. It contains also basic WebSockets logic used for testing.

Let's split the file up and examine the parts, going from the beginning of the file all the way to the end.

First, the basic imports:

import sys
import os
import asyncio

from aiohttp_wsgi import api
import aiohttp

Then, the WebSockets handler for aiohttp.

Note how this is an asyncio coroutine. You can also see the "yield from", which means that at such a point the execution can go elsewhere until the stuff that is yielded from finishes whatever it is doing. Once it does finish, the loop continues from the yield from line. On the next iteration, the same thing happens - there is no blocking for ws.receive_str() to finish.

Note how the send_str doesn't have a yield from.

def ws_testing(request):
    # websockets aiohttp req-resp
    ws = aiohttp.web.WebSocketResponse()

    ws.send_str("Initial server hello")
    while True:
            data = yield from ws.receive_str()
            if data == 'close':
                ws.send_str(data + ' says the server')
        except Exception as exc:
            print(exc.code, exc.message)
            return ws

Django setup for aiohttp is next. It's all in all quite simple - just 2-3 lines, thanks to aiohttp-wsgi. With the Django WSGI application instance, the aiohttp server is configured with the chosen port and host and told to run the Django WSGI application.

# Django setup
sys.path.append("%s/yourdjangoapp" % (os.getcwd()))
from yourdjangoapp import wsgi

loop = asyncio.get_event_loop()
server, app = api.configure_server(

The default routes come from the Django parts (urls.py). Let's add another route for the WebSockets. Django does not know about this routing since it happens in the aiohttp level.

# paths handled as WebSockets
app.router.add_route('GET', '/ws', ws_testing)

So what did we do so far? At this point we made "/" go to the Django app, via WSGI. The "/ws" goes to the WebSocket testing method, which simply first sends a message and then starts echoing all client messages back.

Next, a basic event loop is started to loop forever.

except KeyboardInterrupt:
    api.close_server(server, app, loop=loop)

print("All done, post cleanup stuff happens here")

There is no "post cleanup stuff", something could happen there but now it's just a placeholder.

And that's it for the httpd.py.

Nginx configuration

Note, here I assume your username is bob, the nginx files are located under /home/bob/nginx/ and your host IP is

Note: in case you're wondering, the "lh" in the SSL keys means "localhost".

SSL setup

If you need a refresher in how to make an openssl setup for nginx, refer to this tutorial. As a summary:

  • cd ~bob/nginx/ssl
  • openssl genrsa -des3 -out lh.key 2048
  • openssl req -new -key lh.key -out lh.csr
  • Fill in the information
  • Remove the passphrase
    • cp lh.key lh.key.orig
    • openssl rsa -in lh.key.orig -out lh.key
  • openssl x509 -req -days 1500 -in lh.csr -signkey lh.key -out lh.crt
  • Point nginx to the .key and the .crt

That's it for the SSL setup.


worker_processes  10;
error_log         /home/bob/nginx/log/error.log;
pid               /home/bob/nginx/nginx.pid;
worker_rlimit_nofile 8192;

events {
    worker_connections  4096;

http {
    server_tokens     off;

    upstream wsgitest {

    server {
        listen 8443;

        error_log         /home/bob/nginx/log/error-lh.log;
        access_log        /home/bob/nginx/log/access-lh.log;

        root /home/bob/nginx/siteroot;

        # django + ws upgrade
        location / {
            proxy_pass http://wsgitest/;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";

        ssl on;
            ssl_certificate     /home/bob/nginx/ssl/lh.crt;
            ssl_certificate_key /home/bob/nginx/ssl/lh.key;

    # redirect http --> https
    server {
        error_log         /home/bob/nginx/log/error-lh.log;
        access_log        /home/bob/nginx/log/access-lh.log;
        listen 8000;
        return 301 https://$host:8443/$request_uri;

Testing it

First, start nginx and the aiohttp WSGI front-end:

nginx -c ~bob/nginx/nginx.conf
python3 httpd.py

Then point your browser to Open the developer console. In Firefox the default shortcut for this is Shift-Control-K.

You should see the HTML output from the Django app, and in the console the WebSockets output ping-ponging every 2 seconds.

What's next

  • Stress-test the WS. Thor seemed to break the sort of echoing WebSockets as we have here after a few hundred connections.
  • Stress-test, somehow, a reasonable mix of WS + HTTPS to know where we're at.
  • Profile, make it better.
  • Build something on it!