As real-time web applications gain popularity, WebSockets have become essential for their development. Gone are the days of constantly refreshing for server updates. Modern web applications can receive real-time updates without polling the server – servers push updates as they occur. Robust web frameworks now include built-in WebSocket support. For instance, Ruby on Rails 5 went a step further by incorporating action cables.
Python boasts numerous popular web frameworks. Frameworks like Django provide almost everything needed to build web applications, and any missing features can be added using one of the thousands of available Django plugins. However, managing long-lived connections in Python, or most of its web frameworks, can quickly become challenging. The threaded model and global interpreter lock are often seen as Python’s weaknesses in this regard.
However, this is changing. With recent Python 3 features and existing frameworks like Tornado, handling long-lived connections is no longer an obstacle. Tornado offers web server capabilities in Python specifically designed for managing persistent connections.
This article will guide you through building a basic WebSocket server in Python using Tornado. Our demo application will enable uploading a tab-separated values (TSV) file, parsing it, and making its contents accessible at a unique URL.
Tornado and WebSockets
Tornado is an asynchronous network library specializing in event-driven networking. Its ability to handle tens of thousands of concurrent open connections makes it ideal for managing numerous WebSocket connections on a single node. WebSocket, a protocol enabling full-duplex communication over a single TCP connection, establishes a stateful web connection through its open socket, facilitating real-time data exchange with the server. By maintaining client states, the server simplifies the implementation of real-time applications like chat apps and web games using WebSockets.
WebSockets are designed for implementation in web browsers and servers, with support across all major browsers. Once a connection is established, messages can be exchanged multiple times before closure.
Installing Tornado is straightforward. It’s listed in PyPI and can be installed via pip or easy_install:
| |
Tornado includes its own WebSocket implementation, which is all we’ll need for this article.
WebSockets in Action
One of WebSocket’s strengths is its stateful nature, which alters traditional client-server communication paradigms. A specific use case is when the server handles lengthy processes and streams results back to the client incrementally.
In our example, users can upload a file via WebSocket. The server stores the parsed file in memory for the connection’s duration. Upon request, the server sends portions of the file to the frontend. Moreover, the file will be accessible via a URL, allowing multiple users to view it. If another file is uploaded to the same URL, everyone viewing it will see the update in real time.
We’ll use AngularJS on the frontend. This framework and its libraries simplify file uploads and pagination. However, we’ll use standard JavaScript functions for anything related to WebSockets.
This application is divided into three files:
- parser.py: Contains our Tornado server and request handlers.
- templates/index.html: Frontend HTML template.
- static/parser.js: For our frontend JavaScript.
Establishing a WebSocket Connection
On the frontend, a WebSocket connection is established by creating a WebSocket object:
| |
This is done on page load. Once a WebSocket object is created, event handlers are attached for three key events:
- open: Triggered when a connection is established.
- message: Triggered when a message is received from the server.
- close: Triggered when a connection is closed.
| |
These event handlers don’t automatically trigger AngularJS’s $scope lifecycle, so the handler function’s contents need to be wrapped in $apply. For AngularJS applications, dedicated packages are available [https://www.npmjs.com/package/angular-websocket] to streamline WebSocket integration.
Note that dropped WebSocket connections aren’t reestablished automatically; the application must attempt reconnections when the close event handler is triggered. This is beyond the scope of this article.
Choosing a File for Upload
Since we’re building a single-page application with AngularJS, traditional file upload methods won’t work. We’ll use Danial Farid’s ng-file-upload library to simplify this. With this library, all we need is a button in our frontend template with specific AngularJS directives:
| |
The library allows us to define acceptable file extensions and sizes. Clicking this button, like any <input type=”file”> element, opens the standard file picker.
Uploading the File
When transferring binary data, you can choose between array buffers and blobs. For raw data like images, use blobs and handle them appropriately on the server. Array buffers are for fixed-length binary data, and a text file like TSV can be transferred as a byte string. This code snippet demonstrates file uploading using array buffers.
| |
The ng-file-upload directive provides an uploadFile function. Here, you transform the file into an array buffer using a FileReader and send it via WebSocket.
Note that uploading large files via WebSocket after reading them into array buffers might not be ideal, as it can consume significant memory, leading to performance issues.
Receiving the File on the Server

Tornado determines the message type using the 4-bit opcode and returns a string for binary data and Unicode for text.
| |
In the Tornado web server, array buffers are received as strings.
In this case, we expect TSV content. The file is parsed and converted into a dictionary. Of course, real-world applications would handle arbitrary uploads more robustly.
| |
Requesting a Page
Since we aim to display uploaded TSV data in paginated chunks, we need a way to request specific pages. For simplicity, we’ll use the same WebSocket connection to send page numbers to the server.
| |
The server receives this message as Unicode:
| |
Responding with a dictionary from a Tornado WebSocket server automatically encodes it in JSON format. Therefore, we can directly send a dictionary containing 100 rows of content.
Sharing Access
To enable multiple users to access the same upload, we need unique upload identifiers. When a user connects to the server via WebSocket, a random UUID is generated and assigned to their connection.
| |
uuid.uuid4() generates a random UUID, and str() converts it into a standard hexadecimal string.
When a user with a UUID connects, a corresponding FileHandler instance is added to a dictionary with the UUID as the key. This instance is removed when the connection closes.
| |
Concurrent client additions or removals might cause the clients dictionary to throw a KeyError. Tornado, being asynchronous, provides locking mechanisms for synchronization. A simple coroutine lock is suitable for managing this client dictionary.
If any user uploads a file or navigates between pages, all users sharing the same UUID see the same content.
| |
Running Behind Nginx
Implementing WebSockets is straightforward, but deploying them in production environments requires careful consideration. While Tornado, as a web server, can handle user requests directly, deploying it behind Nginx often proves more advantageous. However, using WebSockets through Nginx requires a little extra configuration:
| |
The two proxy_set_header directives ensure that Nginx correctly forwards the necessary headers to the backend servers for upgrading the connection to WebSocket.
Looking Ahead
This article demonstrated a simple Python web application leveraging WebSockets for persistent server-client connections. Modern asynchronous frameworks like Tornado make managing tens of thousands of concurrent open connections in Python entirely feasible.
While certain aspects of this demo application could have been implemented differently, it hopefully illustrated the use of WebSockets within the https://www.toptal.com/tornado framework. The source code for the demo application is available available on GitHub.