PYTHON-SOCKETIO(1)

๐Ÿ’– python-socketio ๐Ÿ’–

okay bestie, welcome to the python-socketio manual page (that's PYTHON-SOCKETIO(1) if you're keeping score, in the python-socketio section of the manual). This project implements Socket.IO clients and servers that can run standalone or integrated with a variety of Python web frameworks. no cap.


โœจ GETTING STARTED โœจ

๐ŸŒธ What is Socket.IO?

Socket.IO is a transport protocol that enables real-time bidirectional event-based communication between clients (typically, though not always, web browsers) and a server. The official implementations of the client and server components are written in JavaScript. This package provides Python implementations of both, each with standard and asyncio variants. it's giving everything. ๐Ÿ’…

๐Ÿฆ‹ Version compatibility

okay so here's the thing โ€” the Socket.IO protocol has been through a number of revisions, and some of these introduced backward incompatible changes, which means that the client and the server must use compatible versions for everything to work. fr fr.

If you are using the Python client and server, the easiest way to ensure compatibility is to use the same version of this package for the client and the server. If you are using this package with a different client or server, then you must ensure the versions are compatible.

The version compatibility chart below maps versions of this package to versions of the JavaScript reference implementation and the versions of the Socket.IO and Engine.IO protocols. (it's a whole table in the original, so here it is as a lil list, but every number is exactly the same, promise ๐Ÿซถ)

  • JavaScript Socket.IO version 0.9.x โ€” Socket.IO protocol revision 1, 2 โ€” Engine.IO protocol revision 1, 2 โ€” python-socketio version: Not supported โ€” python-engineio version: Not supported
  • JavaScript Socket.IO version 1.x and 2.x โ€” Socket.IO protocol revision 3, 4 โ€” Engine.IO protocol revision 3 โ€” python-socketio version: 4.x โ€” python-engineio version: 3.x
  • JavaScript Socket.IO version 3.x and 4.x โ€” Socket.IO protocol revision 5 โ€” Engine.IO protocol revision 4 โ€” python-socketio version: 5.x โ€” python-engineio version: 4.x

๐Ÿ’œ Client Examples

The example that follows shows a simple Python client:

import socketio

sio = socketio.Client()

@sio.event
def connect():
    print('connection established')

@sio.event
def my_message(data):
    print('message received with ', data)
    sio.emit('my response', {'response': 'my response'})

@sio.event
def disconnect():
    print('disconnected from server')

sio.connect('http://localhost:5000')
sio.wait()

Below is a similar client, coded for asyncio (Python 3.5+ only):

import asyncio
import socketio

sio = socketio.AsyncClient()

@sio.event
async def connect():
    print('connection established')

@sio.event
async def my_message(data):
    print('message received with ', data)
    await sio.emit('my response', {'response': 'my response'})

@sio.event
async def disconnect():
    print('disconnected from server')

async def main():
    await sio.connect('http://localhost:5000')
    await sio.wait()

if __name__ == '__main__':
    asyncio.run(main())

๐ŸŽ€ Client Features

  • Can connect to other Socket.IO servers that are compatible with the JavaScript Socket.IO reference server.
  • Compatible with Python 3.8+.
  • Two versions of the client, one for standard Python and another for asyncio.
  • Uses an event-based architecture implemented with decorators that hides the details of the protocol.
  • Implements HTTP long-polling and WebSocket transports.
  • Automatically reconnects to the server if the connection is dropped. ๐Ÿ’… iconic behaviour honestly

๐ŸŒˆ Server Examples

The following application is a basic server example that uses the Eventlet asynchronous server:

import eventlet
import socketio

sio = socketio.Server()
app = socketio.WSGIApp(sio, static_files={
    '/': {'content_type': 'text/html', 'filename': 'index.html'}
})

@sio.event
def connect(sid, environ):
    print('connect ', sid)

@sio.event
def my_message(sid, data):
    print('message ', data)

@sio.event
def disconnect(sid):
    print('disconnect ', sid)

if __name__ == '__main__':
    eventlet.wsgi.server(eventlet.listen(('', 5000)), app)

Below is a similar application, coded for asyncio (Python 3.5+ only) and the Uvicorn web server:

from aiohttp import web
import socketio

sio = socketio.AsyncServer()
app = web.Application()
sio.attach(app)

async def index(request):
    """Serve the client-side application."""
    with open('index.html') as f:
        return web.Response(text=f.read(), content_type='text/html')

@sio.event
def connect(sid, environ):
    print("connect ", sid)

@sio.event
async def chat_message(sid, data):
    print("message ", data)

@sio.event
def disconnect(sid):
    print('disconnect ', sid)

app.router.add_static('/static', 'static')
app.router.add_get('/', index)

if __name__ == '__main__':
    web.run_app(app)

โญ Server Features

  • Can connect to servers running other Socket.IO clients that are compatible with the JavaScript reference client.
  • Compatible with Python 3.8+.
  • Two versions of the server, one for standard Python and another for asyncio.
  • Supports large number of clients even on modest hardware due to being asynchronous. ate. ๐Ÿ‘‘
  • Can be hosted on any WSGI (https://wsgi.readthedocs.io/en/latest/index.html) or ASGI (https://asgi.readthedocs.io/en/latest/) web server including Gunicorn (https://gunicorn.org/), Uvicorn (https://github.com/encode/uvicorn), eventlet (http://eventlet.net/) and gevent (http://www.gevent.org).
  • Can be integrated with WSGI applications written in frameworks such as Flask, Django, etc.
  • Can be integrated with aiohttp (http://aiohttp.readthedocs.io/), FastAPI (https://fastapi.tiangolo.com/), sanic (http://sanic.readthedocs.io/) and tornado (http://www.tornadoweb.org/) asyncio applications.
  • Broadcasting of messages to all connected clients, or to subsets of them assigned to "rooms".
  • Optional support for multiple servers, connected through a messaging queue such as Redis or RabbitMQ.
  • Send messages to clients from external processes, such as Celery workers or auxiliary scripts.
  • Event-based architecture implemented with decorators that hides the details of the protocol.
  • Support for HTTP long-polling and WebSocket transports.
  • Support for XHR2 and XHR browsers.
  • Support for text and binary messages.
  • Support for gzip and deflate HTTP compression.
  • Configurable CORS responses, to avoid cross-origin problems with browsers.

๐Ÿ‘‘ THE SOCKET.IO CLIENTS

This package contains two Socket.IO clients:

  • a "simple" client, which provides a straightforward API that is sufficient for most applications
  • an "event-driven" client, which provides access to all the features of the Socket.IO protocol

Each of these clients comes in two variants: one for the standard Python library, and another for asynchronous applications built with the asyncio package. two flavours, same slay. ๐Ÿ“

๐Ÿ’… Installation

To install the standard Python client along with its dependencies, use the following command:

pip install "python-socketio[client]"

If instead you plan on using the asyncio client, then use this:

pip install "python-socketio[asyncio_client]"

๐Ÿงš Using the Simple Client

The advantage of the simple client is that it abstracts away the logic required to maintain a Socket.IO connection. This client handles disconnections and reconnections in a completely transparent way, without adding any complexity to the application. lowkey the bestie that handles everything for you.

๐ŸŒธ Creating a Client Instance

The easiest way to create a Socket.IO client is to use the context manager interface:

import socketio

# standard Python
with socketio.SimpleClient() as sio:
    # ... connect to a server and use the client
    # ... no need to manually disconnect!

# asyncio
async with socketio.AsyncSimpleClient() as sio:
    # ... connect to a server and use the client
    # ... no need to manually disconnect!

With this usage the context manager will ensure that the client is properly disconnected before exiting the with or async with block.

If preferred, a client can be manually instantiated:

import socketio

# standard Python
sio = socketio.SimpleClient()

# asyncio
sio = socketio.AsyncSimpleClient()

๐Ÿฆ‹ Connecting to a Server

The connection to a server is established by calling the connect() method:

sio.connect('http://localhost:5000')

In the case of the asyncio client, the method is a coroutine:

await sio.connect('http://localhost:5000')

By default the client first connects to the server using the long-polling transport, and then attempts to upgrade the connection to use WebSocket. To connect directly using WebSocket, use the transports argument:

sio.connect('http://localhost:5000', transports=['websocket'])

Upon connection, the server assigns the client a unique session identifier. The application can find this identifier in the sid attribute:

print('my sid is', sio.sid)

The Socket.IO transport that is used in the connection can be obtained from the transport attribute:

print('my transport is', sio.transport)

The transport is given as a string, and can be either 'websocket' or 'polling'.

๐Ÿ”ฅ TLS/SSL Support

The client supports TLS/SSL connections. To enable it, use a https:// connection URL:

sio.connect('https://example.com')

Or when using asyncio:

await sio.connect('https://example.com')

The client verifies server certificates by default, no cap. If you wanna mess with that behavior, go consult the documentation for the event-driven client โ€” it's all laid out there.


โœจ Emitting Events โœจ

Bestie, your client can shoot an event at the server with the emit() method:

sio.emit('my message', {'foo': 'bar'})

And if you're on asyncio, it's a coroutine instead:

await sio.emit('my message', {'foo': 'bar'})

So the args you hand over are the name of the event to emit plus the optional data that gets passed on to the server. That data can be a str, bytes, dict, list or tuple. ๐ŸŒˆ When you're sending a list or a tuple, the elements inside it need to be any of the allowed types except tuple. And when you do use a tuple? The elements of that tuple get passed as individual arguments to the server-side event handler function. iykyk ๐Ÿ’…

๐ŸŒธ Receiving Events ๐ŸŒธ

Your client can also sit there and wait for the server to emit an event, using the receive() method:

event = sio.receive()
print(f'received event: "{event[0]}" with arguments {event[1:]}')

When you're on asyncio, this method needs to be awaited:

event = await sio.receive()
print(f'received event: "{event[0]}" with arguments {event[1:]}')

What receive() gives back is a list. The first element of that list is the event name, and the remaining elements are the arguments the server passed along. ๐Ÿซถ

With the usage above, receive() will only return when an event actually arrives from the server. But you can pass an optional timeout in seconds so your client doesn't just wait forever like a ghoster:

from socketio.exceptions import TimeoutError

try:
    event = sio.receive(timeout=5)
except TimeoutError:
    print('timed out waiting for event')
else:
    print('received event:', event)

Or with asyncio, same energy:

from socketio.exceptions import TimeoutError

try:
    event = await sio.receive(timeout=5)
except TimeoutError:
    print('timed out waiting for event')
else:
    print('received event:', event)

๐Ÿ’… Disconnecting from the Server ๐Ÿ’…

At literally any time, your client can request to be disconnected from the server by calling the disconnect() method:

sio.disconnect()

For the asyncio client this one is a coroutine:

await sio.disconnect()

๐Ÿฆ‹ Debugging and Troubleshooting ๐Ÿฆ‹

To help you debug your issues, the client can be configured to output logs straight to the terminal:

import socketio

# standard Python
sio = socketio.Client(logger=True, engineio_logger=True)

# asyncio
sio = socketio.AsyncClient(logger=True, engineio_logger=True)

The logger argument controls logging related to the Socket.IO protocol, while engineio_logger controls the logs that originate in the low-level Engine.IO transport. What you hand to these arguments decides the logging behavior:

  • True: enables log output to stderr at the INFO level.
  • False: enables log output to stderr at the ERROR level. This is the default.
  • A logging.Logger instance: uses the logger you provided, with no additional configuration.

Logging can help you figure out the cause of connection problems, unexpected disconnections and other issues. ๐Ÿ”ฅ


๐Ÿ‘‘ Using the Event-Driven Client ๐Ÿ‘‘

โœจ Creating a Client Instance โœจ

To instantiate a Socket.IO client, you literally just create an instance of the appropriate client class:

import socketio

# standard Python
sio = socketio.Client()

# asyncio
sio = socketio.AsyncClient()

๐ŸŽ€ Defining Event Handlers ๐ŸŽ€

The Socket.IO protocol is event based, periodt. When a server wants to communicate with a client it emits an event. Every event has a name and a list of arguments. Your client registers event handler functions using the socketio.Client.event() or socketio.Client.on() decorators:

@sio.event
def message(data):
    print('I received a message!')

@sio.on('my message')
def on_message(data):
    print('I received a message!')

In the first example, the event name is taken from the name of the handler function. The second example is a lil more verbose, but it lets the event name be different from the function name โ€” or include characters that are illegal in function names, like spaces. ๐Ÿ’€

For the asyncio client, event handlers can be regular functions like above, or they can be coroutines too:

@sio.event
async def message(data):
    print('I received a message!')

If the server includes arguments with an event, those get passed to the handler function as arguments. ๐ŸŒธ

๐ŸŒˆ Catch-All Event and Namespace Handlers ๐ŸŒˆ

A "catch-all" event handler gets invoked for any events that don't have an event handler. You can define one by using '*' as the event name:

@sio.on('*')
def any_event(event, sid, data):
     pass

Asyncio servers can also use a coroutine:

@sio.on('*')
async def any_event(event, sid, data):
    pass

A catch-all event handler receives the event name as its first argument. The remaining arguments are the same as for a regular event handler. โœจ

The connect and disconnect events have to be defined explicitly โ€” they are not invoked on a catch-all event handler.

Similarily, a "catch-all" namespace handler gets invoked for any connected namespaces that don't have an explicitly defined event handler. Just like with catch-all events, '*' is used in place of a namespace:

@sio.on('my_event', namespace='*')
def my_event_any_namespace(namespace, sid, data):
    pass

For these events, the namespace is passed as the first argument, followed by the regular arguments of the event. ๐Ÿซถ

Lastly, it's also possible to define a "catch-all" handler for all events on all namespaces:

@sio.on('*', namespace='*')
def any_event_any_namespace(event, namespace, sid, data):
    pass

Event handlers with catch-all events and namespaces receive the event name and the namespace as their first and second arguments. ๐Ÿ‘‘

๐Ÿ’– Connect, Connect Error and Disconnect Event Handlers ๐Ÿ’–

The connect, connect_error and disconnect events are special โ€” they're invoked automatically when a client connects or disconnects from the server:

@sio.event
def connect():
    print("I'm connected!")

@sio.event
def connect_error(data):
    print("The connection failed!")

@sio.event
def disconnect(reason):
    print("I'm disconnected! reason:", reason)

The connect_error handler is invoked when a connection attempt fails. If the server provides arguments, those get passed on to the handler. The server can use an argument to hand your client info about the connection failure. ๐Ÿ˜ญ

The disconnect handler is invoked for application initiated disconnects, server initiated disconnects, or accidental disconnects โ€” for example, due to networking failures. In the case of an accidental disconnection, the client is gonna attempt to reconnect immediately after invoking the disconnect handler. And as soon as the connection is re-established, the connect handler gets invoked once again. The handler receives a reason argument which tells you the cause of the disconnection:

@sio.event
def disconnect(reason):
    if reason == sio.reason.CLIENT_DISCONNECT:
        print('the client disconnected')
    elif reason == sio.reason.SERVER_DISCONNECT:
        print('the server disconnected the client')
    else:
        print('disconnect reason:', reason)

See the The socketio.Client.reason attribute for a list of possible disconnection reasons. ๐Ÿฆ‹

The connect, connect_error and disconnect events have to be defined explicitly โ€” they are not invoked on a catch-all event handler.

๐Ÿ“ Connecting to a Server ๐Ÿ“

The connection to a server gets established by calling the connect() method:

sio.connect('http://localhost:5000')

In the case of the asyncio client, the method is a coroutine:

await sio.connect('http://localhost:5000')

Upon connection, the server assigns your client a unique session identifier. Your app can find this identifier in the sid attribute:

print('my sid is', sio.sid)

The Socket.IO transport used in the connection can be grabbed from the transport attribute:

print('my transport is', sio.transport)

The transport is given as a string, and it can be either 'websocket' or 'polling'. ๐Ÿ’…

๐Ÿงš TLS/SSL Support ๐Ÿงš

The client supports TLS/SSL connections. To enable it, use an https:// connection URL:

sio.connect('https://example.com')

Or when you're using asyncio:

await sio.connect('https://example.com')

The client will verify the server certificate by default. To disable certificate verification, or to use other less common options such as client certificates, the client must be initialized with a custom HTTP session object that's configured with the TLS/SSL options you want. ๐Ÿ”ฅ

The following example disables server certificate verification, which can be useful when connecting to a server that uses a self-signed certificate:

http_session = requests.Session()
http_session.verify = False
sio = socketio.Client(http_session=http_session)
sio.connect('https://example.com')

And when using asyncio:

connector = aiohttp.TCPConnector(ssl=False)
http_session = aiohttp.ClientSession(connector=connector)
sio = socketio.AsyncClient(http_session=http_session)
await sio.connect('https://example.com')

Instead of disabling certificate verification, you can provide a custom certificate authority bundle to verify the certificate against:

http_session = requests.Session()
http_session.verify = '/path/to/ca.pem'
sio = socketio.Client(http_session=http_session)
sio.connect('https://example.com')

And for asyncio:

ssl_context = ssl.create_default_context()
ssl_context.load_verify_locations('/path/to/ca.pem')
connector = aiohttp.TCPConnector(ssl=ssl_context)
http_session = aiohttp.ClientSession(connector=connector)
sio = socketio.AsyncClient(http_session=http_session)
await sio.connect('https://example.com')

Below you can see how to use a client certificate to authenticate against the server:

http_session = requests.Session()
http_session.cert = ('/path/to/client/cert.pem', '/path/to/client/key.pem')
sio = socketio.Client(http_session=http_session)
sio.connect('https://example.com')

And for asyncio:

ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain('/path/to/client/cert.pem',
                            '/path/to/client/key.pem')
connector = aiohttp.TCPConnector(ssl=ssl_context)
http_session = aiohttp.ClientSession(connector=connector)
sio = socketio.AsyncClient(http_session=http_session)
await sio.connect('https://example.com')

โœจ Emitting Events โœจ

okay so your client wants to send something out into the world? that's an emit(), bestie, and it's giving effortless:

sio.emit('my message', {'foo': 'bar'})

or if you're living that asyncio life, as a coroutine:

await sio.emit('my message', {'foo': 'bar'})

the stuff you hand over to this method is the name of the event you're emitting, plus the optional data that gets passed along to the server. that data can be a str, bytes, dict, list or tuple ๐Ÿซถ

heads up on the list/tuple situation, because it's lowkey important: when you send a list or a tuple, every element inside it has to be one of the allowed types โ€” except tuple. no nesting tuples in there, bestie, it's not the vibe.

and when you use a tuple? the elements of that tuple get passed as individual arguments to the server-side event handler function. iconic behaviour honestly.

the emit() method can be called inside an event handler (as a response to a server event) or literally anywhere else in your application, background tasks included. main character energy, no restrictions ๐Ÿ’…


๐Ÿ’– Event Callbacks ๐Ÿ’–

so when a server emits an event to a client, it can optionally hand over a callback function โ€” a little "yes queen, I processed your event" acknowledgment. all of that is managed by the server, but the client gets to decide what gets sent back: you can provide a list of return values that get passed to the callback function the server set up. and how? simply by returning the values you want from your handler function:

@sio.event
def my_event(sid, data):
    # handle the message
    return "OK", 123

and it goes the other way too ๐ŸŒ€ the client can ask for a callback function to be invoked once the server has processed an event. the socketio.Server.emit() method has an optional callback argument you can set to a callable. if you give it one, that callable gets invoked after the server processes the event, and any values the server handler returns get passed as arguments to it. chef's kiss.


๐ŸŒˆ Namespaces ๐ŸŒˆ

the Socket.IO protocol supports multiple logical connections, all multiplexed on the same physical connection (yes, that's as cute as it sounds). clients can open multiple connections by specifying a different namespace on each one.

namespaces use a path syntax starting with a forward slash /. and the client can hand over a list of namespaces right in the connect() call. for example, this lil number creates two logical connections โ€” the default one, plus a second one under the /chat namespace:

sio.connect('http://localhost:5000', namespaces=['/chat'])

to define event handlers on a namespace, you just add the namespace argument to the matching decorator:

@sio.event(namespace='/chat')
def my_custom_event(sid, data):
    pass

@sio.on('connect', namespace='/chat')
def on_connect():
    print("I'm connected to the /chat namespace!")

and same energy for emitting โ€” the client can emit an event to the server on a namespace by providing it in the emit() call:

sio.emit('my message', {'foo': 'bar'}, namespace='/chat')
psst โ€” if the namespaces argument of the connect() call isn't given, then any namespaces you use in your event handlers are automatically connected. convenient? yes. slay? also yes. โœจ

๐Ÿฆ‹ Class-Based Namespaces ๐Ÿฆ‹

as an alternative to the decorator-based event handlers, you can build the event handlers that belong to a namespace as methods of a subclass of socketio.ClientNamespace:

class MyCustomNamespace(socketio.ClientNamespace):
    def on_connect(self):
        pass

    def on_disconnect(self, reason):
        pass

    def on_my_event(self, data):
        self.emit('my_response', data)

sio.register_namespace(MyCustomNamespace('/chat'))

for asyncio based servers, namespaces must inherit from socketio.AsyncClientNamespace, and you can define event handlers as coroutines if that's what you want ๐Ÿ’œ:

class MyCustomNamespace(socketio.AsyncClientNamespace):
    def on_connect(self):
        pass

    def on_disconnect(self, reason):
        pass

    async def on_my_event(self, data):
        await self.emit('my_response', data)

sio.register_namespace(MyCustomNamespace('/chat'))

want a catch-all class-based namespace handler? just pass '*' as the namespace when you register:

sio.register_namespace(MyCustomNamespace('*'))

okay so here's how the dispatch works when you're using class-based namespaces: any events received by the client get dispatched to a method named as the event name with the on_ prefix. so for example, event my_event will be handled by a method named on_my_event ๐ŸŽ€

and if an event comes in that has no corresponding method defined in the namespace class? then the event is ignored. no drama, just silence. also โ€” all event names used in class-based namespaces must use characters that are legal in method names. keep it valid, bestie.

as a convenience to methods defined in a class-based namespace, the namespace instance includes versions of several of the methods in the socketio.Client and socketio.AsyncClient classes that default to the proper namespace when the namespace argument isn't given. so you don't have to keep typing it out. we love efficiency ๐Ÿซถ

โš ๏ธ edge case alert: if an event has a handler in a class-based namespace and also a decorator-based function handler, then only the standalone function handler is invoked. don't ask me why, that's just the rules.

๐Ÿ’€ Disconnecting from the Server ๐Ÿ’€

at any time, the client can request to be disconnected from the server by calling the disconnect() method:

sio.disconnect()

and for the asyncio client, it's a coroutine:

await sio.disconnect()

bye bestie ๐Ÿ‘‹


๐ŸŒธ Managing Background Tasks ๐ŸŒธ

when a client connection to the server is established, a few background tasks get spawned to keep the connection alive and handle incoming events. the application running on the main thread is free to do literally whatever work it wants โ€” none of it will prevent the Socket.IO client from functioning. multitasking queen.

if your application has nothing to do on the main thread and just wants to wait until the connection with the server ends, it can call the wait() method:

sio.wait()

or the asyncio version:

await sio.wait()

and for your convenience, there's a helper function to start a custom background task:

def my_background_task(my_argument):
    # do some background work here!
    pass

task = sio.start_background_task(my_background_task, 123)

the arguments you pass to this method are the background function, plus any positional or keyword arguments to invoke that function with.

here's the asyncio version:

async def my_background_task(my_argument):
    # do some background work here!
    pass

task = sio.start_background_task(my_background_task, 123)
๐Ÿšจ important note: this function is not a coroutine, because it does not wait for the background function to end. but the background function itself must be a coroutine. don't mix these up, it's giving runtime error energy.

the sleep() method is a second convenience function, provided for the benefit of applications working with background tasks of their own:

sio.sleep(2)

or for asyncio:

await sio.sleep(2)

the single argument you pass to the method is the number of seconds to sleep for. that's it. that's the whole vibe ๐Ÿ˜ด


๐Ÿ”ฅ Debugging and Troubleshooting ๐Ÿ”ฅ

when things go wrong (and bestie, sometimes they do ๐Ÿ˜ญ), the client can be configured to output logs to the terminal to help you debug:

import socketio

# standard Python
sio = socketio.Client(logger=True, engineio_logger=True)

# asyncio
sio = socketio.AsyncClient(logger=True, engineio_logger=True)

so the logger argument controls logging related to the Socket.IO protocol, while engineio_logger controls logs that originate in the low-level Engine.IO transport. two different layers, two different switches, keep track ๐Ÿ’…

these arguments can be set to True to output logs to stderr, or to an object compatible with Python's logging package where the logs should be emitted to. and a value of False disables logging entirely.

logging can help you identify the cause of connection problems, unexpected disconnections and other issues. it's the detective work, fr fr ๐Ÿ”


๐Ÿ‘‘ THE SOCKET.IO SERVER ๐Ÿ‘‘

this package contains two Socket.IO servers:

  • the socketio.Server() class creates a server compatible with the Python standard library.
  • the socketio.AsyncServer() class creates a server compatible with the asyncio package.

the methods in the two servers are the same โ€” the only difference is that in the asyncio server, most methods are implemented as coroutines. same flavour, different packaging ๐ŸŽ€


โญ Installation โญ

to install the Socket.IO server along with its dependencies, use the following command:

pip install python-socketio

one line. that's it. go off โœจ


๐Ÿ’œ Creating a Server Instance ๐Ÿ’œ

a Socket.IO server is an instance of class socketio.Server:

import socketio

# create a Socket.IO server
sio = socketio.Server()

for asyncio based servers, the socketio.AsyncServer class provides the same functionality, but in a coroutine friendly format:

import socketio

# create a Socket.IO server
sio = socketio.AsyncServer()

pick your fighter, both are valid ๐Ÿซถ


๐ŸŽ€ Running the Server ๐ŸŽ€

to run the Socket.IO application, you need to configure a web server to receive incoming requests from clients and forward them to the Socket.IO server instance. to simplify this task, several integrations are available, including support for the WSGI (https://wsgi.readthedocs.io/en/latest/what.html) and ASGI (https://asgi.readthedocs.io/en/latest/) standards.


๐Ÿฆ‹ Running as a WSGI Application ๐Ÿฆ‹

to configure the Socket.IO server as a WSGI application, wrap the server instance with the socketio.WSGIApp class:

# wrap with a WSGI application
app = socketio.WSGIApp(sio)

the resulting WSGI application can be executed with supported WSGI servers such as Werkzeug (https://werkzeug.palletsprojects.com) for development and Gunicorn (https://gunicorn.org/) for production. dev vibe and prod vibe, both covered ๐Ÿ’…

when combining Socket.IO with a web application written with a WSGI framework such as Flask or Django, the WSGIApp class can wrap both applications together and route traffic to them:

from mywebapp import app  # a Flask, Django, etc. application
app = socketio.WSGIApp(sio, app)

two apps, one wrapper, zero stress โœจ


๐ŸŒˆ Running as an ASGI Application ๐ŸŒˆ

to configure the Socket.IO server as an ASGI application, wrap the server instance with the socketio.ASGIApp class:

# wrap with ASGI application
app = socketio.ASGIApp(sio)

the resulting ASGI application can be executed with an ASGI compliant web server โ€” for example Uvicorn (https://www.uvicorn.org/).

Socket.IO can also be combined with a web application written with an ASGI web framework such as FastAPI. in that case, the ASGIApp class can wrap both applications together and route traffic to them:

from mywebapp import app  # a FastAPI or other ASGI application
app = socketio.ASGIApp(sio, app)

same energy as the WSGI one, just the async flavour ๐ŸŒธ


๐Ÿ“ Serving Static Files ๐Ÿ“

the Socket.IO server can be configured to serve static files to clients. this is particularly useful to deliver HTML, CSS and JavaScript files to clients when this package is used without a companion web framework. no framework? no problem, we got you ๐Ÿซถ

static files are configured with a Python dictionary in which each key/value pair is a static file mapping rule. in its simplest form, this dictionary has one or more static file URLs as keys, and the corresponding files in the server as values:

static_files = {
    '/': 'latency.html',
    '/static/socket.io.js': 'static/socket.io.js',
    '/static/style.css': 'static/style.css',
}

So with that config, bestie, when the server gets a request for / (that's the root URL, the front door of the whole thing) it hands back the contents of the file latency.html sitting in the current directory, and it picks a content type for it based on the file extension โ€” in this case text/html. Auto-detected, no cap. ๐Ÿ’…

The extensions that get automatically recognized and matched to the right content type are .html, .css, .js, .json, .jpg, .png, .gif and .txt. Everything else โ€” other extensions, or literally no extension at all โ€” gets the default application/octet-stream content type. That's just how it is. ๐Ÿ˜Œ

And if you're feeling picky, you can hand-set an explicit content type for a static file like this:

static_files = {
    '/': {'filename': 'latency.html', 'content_type': 'text/plain'},
}

You can also configure a WHOLE directory in one single rule, so every file in it gets served as a static file. One line, whole folder, iconic behavior:

static_files = {
    '/static': './public',
}

In that example, any file whose URL starts with /static gets served straight out of the public folder in the current directory. So like, the URL /static/index.html returns the local file ./public/index.html, and the URL /static/css/styles.css returns the local file ./public/css/styles.css. It's giving mapping. โœจ

Now, if a URL ends in a /, a default filename of index.html gets appended to it. Going back to that last example โ€” a request for the /static/ URL would return the local file ./public/index.html. And you can change which default filename gets served for slash-ending URLs by setting it in the static files dictionary with an empty key:

static_files = {
    '/static': './public',
    '': 'image.gif',
}

With that configuration, a request for /static/ would return the local file ./public/image.gif. And yes, you can specify a non-standard content type here too if you need it:

static_files = {
    '/static': './public',
    '': {'filename': 'image.gif', 'content_type': 'text/plain'},
}

The static file configuration dictionary gets passed as the static_files argument to the socketio.WSGIApp or socketio.ASGIApp classes:

# for standard WSGI applications
sio = socketio.Server()
app = socketio.WSGIApp(sio, static_files=static_files)

# for asyncio-based ASGI applications
sio = socketio.AsyncServer()
app = socketio.ASGIApp(sio, static_files=static_files)

And the routing precedence in these two classes goes like this, in order, no skipping:

  • First, the path gets checked against the Socket.IO endpoint.
  • Next, the path gets checked against the static file configuration, if there is one.
  • If the path didn't match the Socket.IO endpoint or any static file, control gets passed to the secondary application if one is configured โ€” otherwise you get a 404 error back.
Note: static file serving is intended for development use only, and because of that it lacks important features such as caching. Do not use it in a production environment. I'm serious, bestie, this is a dev toy, not a whole career. ๐Ÿ™ˆ

๐ŸŽ‰ Events

The Socket.IO protocol is event based. When a client wants to talk to the server, or the server wants to talk to one or more clients, they emit an event at the other party. Every event has a name, and an optional list of arguments. That's the whole vibe. ๐Ÿฆ‹

๐Ÿ‘‚ Listening to Events

To receive events from clients, the server application has to register event handler functions. These functions get invoked when the corresponding events are emitted by clients. To register a handler for an event, you use the socketio.Server.event() or socketio.Server.on() decorators:

@sio.event
def my_event(sid, data):
    pass

@sio.on('my custom event')
def another_event(sid, data):
    pass

In the first example the event name is just taken from the name of the handler function. The second one is a lil more verbose, but it lets the event name be different from the function name, or include characters that are illegal in function names โ€” like spaces. Spaces! In a function name! Scandalous. ๐Ÿ’€

For asyncio servers, event handlers can optionally be given as coroutines:

@sio.event
async def my_event(sid, data):
    pass

The sid argument that gets passed to all handlers is the Socket.IO session id โ€” a unique identifier that Socket.IO assigns to each client connection. Every event sent by a given client will carry that same sid value. One client, one sid, periodt. ๐Ÿ‘‘

๐Ÿ”Œ Connect and Disconnect Events

The connect and disconnect events are special โ€” they get invoked automatically when a client connects to or disconnects from the server:

@sio.event
def connect(sid, environ, auth):
    print('connect ', sid)

@sio.event
def disconnect(sid, reason):
    print('disconnect ', sid, reason)

The connect event is the ideal place to do user authentication, and any mapping you need between user entities in the application and the sid that got assigned to the client. Bestie, this is where you do your checks. ๐Ÿซถ

Besides the sid, the connect handler receives environ as an argument โ€” with the request information in standard WSGI format, HTTP headers included. It also receives the auth argument with any authentication details the client passed, or None if the client didn't pass any authentication at all.

After inspecting the arguments, the connect event handler can return False to reject the connection with the client. And sometimes it's useful to pass data back to the client you're rejecting โ€” in that case, instead of returning False, you raise a socketio.exceptions.ConnectionRefusedError exception, and all of its arguments will be sent to the client along with the rejection message:

from socketio.exceptions import ConnectionRefusedError

@sio.event
def connect(sid, environ, auth):
    raise ConnectionRefusedError('authentication failed')

The disconnect handler receives the sid assigned to the client and a reason, which tells you the cause of the disconnection:

@sio.event
def disconnect(sid, reason):
    if reason == sio.reason.CLIENT_DISCONNECT:
        print('the client disconnected')
    elif reason == sio.reason.SERVER_DISCONNECT:
        print('the server disconnected the client')
    else:
        print('disconnect reason:', reason)

See the The socketio.Server.reason attribute section for the full list of possible disconnection reasons. ๐Ÿ‘€

๐Ÿ•ธ๏ธ Catch-All Event Handlers

A "catch-all" event handler gets invoked for any events that don't have an event handler. You define a catch-all handler by using '*' as the event name:

@sio.on('*')
def any_event(event, sid, data):
     pass

Asyncio servers can use a coroutine here too:

@sio.on('*')
async def any_event(event, sid, data):
    pass

A catch-all event handler receives the event name as a first argument. The rest of the arguments are the same as for a regular event handler. Same energy, extra name. โœจ

Note: the connect and disconnect events have to be defined explicitly โ€” they are not invoked on a catch-all event handler. So don't try to sneak them in there. ๐Ÿ™…

๐Ÿ“ค Emitting Events to Clients

Socket.IO is a bidirectional protocol, so at literally any time the server can send an event to its connected clients. The socketio.Server.emit() method is what does this job:

sio.emit('my event', {'data': 'foobar'})

The first argument is the event name, followed by an optional data payload of type str, bytes, list, dict or tuple. When you send a list, dict or tuple, the elements inside are also constrained to those same data types. And when a tuple is sent, its elements get passed as multiple arguments to the client-side event handler function. Sneaky tuple behavior, love that for us. ๐ŸŒธ

That example above sends the event to all the clients that are connected. But sometimes the server only wants to send an event to one particular client. You do that by adding a to argument to the emit call, with the sid of that client:

sio.emit('my event', {'data': 'foobar'}, to=user_sid)

The to argument identifies which client should receive the event, and it's set to the sid value assigned to that client's connection with the server. When to is omitted, the event gets broadcasted to all connected clients. Everyone gets it. Group chat behavior. ๐Ÿ’ฌ

โœ… Acknowledging Events

When a client sends an event to the server, it can optionally request to receive an acknowledgment back from the server. The sending of acknowledgements is automatically managed by the Socket.IO server, but the event handler function can supply a list of values that get passed on to the client with the acknowledgement โ€” simply by returning them:

@sio.event
def my_event(sid, data):
    # handle the message
    return "OK", 123  # <-- client will have these as acknowledgement

๐Ÿ“ฅ Requesting Client Acknowledgements

Similar to how clients can request acknowledgements from the server, when the server is emitting to a single client it can also ask the client to acknowledge the event, and optionally return one or more values as a response. Two-way receipts, we love to see it. ๐Ÿ’Œ

The Socket.IO server supports two ways of working with client acknowledgements. The most convenient method is to swap out socketio.Server.emit() for socketio.Server.call(). The call() method emits the event, then waits until the client sends an acknowledgement, and returns any values the client provided:

response = sio.call('my event', {'data': 'foobar'}, to=user_sid)

A much more primitive acknowledgement solution uses callback functions. The socketio.Server.emit() method has an optional callback argument that can be set to a callable. If you give that argument, the callable gets invoked after the client has processed the event, and any values returned by the client get passed as arguments to that function:

def my_callback():
    print("callback invoked!")

sio.emit('my event', {'data': 'foobar'}, to=user_sid, callback=my_callback)

๐Ÿšช Rooms

To make it easy for the server to emit events to groups of related clients, the application can put its clients into "rooms", and then address messages to those rooms. It's giving little clubhouses. ๐Ÿ 

In previous examples, the to argument of the socketio.SocketIO.emit() method was used to designate one specific client as the recipient of the event. The to argument can also be given the name of a room โ€” and then all the clients that are in that room receive the event. Everybody in the room, no FOMO. ๐ŸŽ€

The application can create as many rooms as it needs and manage which clients are in them using the socketio.Server.enter_room() and socketio.Server.leave_room() methods. Clients can be in as many rooms as needed, and can be moved between rooms whenever necessary.

@sio.event
def begin_chat(sid):
    sio.enter_room(sid, 'chat_users')

@sio.event
def exit_chat(sid):
    sio.leave_room(sid, 'chat_users')

In chat applications it is often desired that an event is broadcasted to all the members of the room except one, which is the originator of the event such as a chat message. The socketio.Server.emit() method provides an optional skip_sid argument to indicate a client that should be skipped during the broadcast.

@sio.event
def my_message(sid, data):
    sio.emit('my reply', data, room='chat_users', skip_sid=sid)

๐ŸŒธ Namespaces ๐ŸŒธ

The Socket.IO protocol supports multiple logical connections, all multiplexed on the same physical connection. Clients can open multiple connections by specifying a different namespace on each. A namespace is given by the client as a pathname following the hostname and port. For example, connecting to http://example.com:8000/chat would open a connection to the namespace /chat.

Each namespace works independently from the others, with separate session IDs (sids), event handlers and rooms. Namespaces can be defined directly in the event handler functions, or they can also be created as classes.

โœจ Decorator-Based Namespaces โœจ

Decorator-based namespaces are regular event handlers that include the namespace argument in their decorator:

@sio.event(namespace='/chat')
def my_custom_event(sid, data):
    pass

@sio.on('my custom event', namespace='/chat')
def my_custom_event(sid, data):
    pass

When emitting an event, the namespace optional argument is used to specify which namespace to send it on. When the namespace argument is omitted, the default Socket.IO namespace, which is named /, is used.

It is important that applications that use multiple namespaces specify the correct namespace when setting up their event handlers and rooms using the optional namespace argument. This argument must also be specified when emitting events under a namespace. Most methods in the socketio.Server class have the optional namespace argument.

๐Ÿฆ‹ Class-Based Namespaces ๐Ÿฆ‹

As an alternative to the decorator-based namespaces, the event handlers that belong to a namespace can be created as methods in a subclass of socketio.Namespace:

class MyCustomNamespace(socketio.Namespace):
    def on_connect(self, sid, environ):
        pass

    def on_disconnect(self, sid, reason):
        pass

    def on_my_event(self, sid, data):
        self.emit('my_response', data)

sio.register_namespace(MyCustomNamespace('/test'))

For asyncio based servers, namespaces must inherit from socketio.AsyncNamespace, and can define event handlers as coroutines if desired:

class MyCustomNamespace(socketio.AsyncNamespace):
    def on_connect(self, sid, environ):
        pass

    def on_disconnect(self, sid, reason):
        pass

    async def on_my_event(self, sid, data):
        await self.emit('my_response', data)

sio.register_namespace(MyCustomNamespace('/test'))

When class-based namespaces are used, any events received by the server are dispatched to a method named as the event name with the on_ prefix. For example, event my_event will be handled by a method named on_my_event. If an event is received for which there is no corresponding method defined in the namespace class, then the event is ignored. All event names used in class-based namespaces must use characters that are legal in method names.

As a convenience to methods defined in a class-based namespace, the namespace instance includes versions of several of the methods in the socketio.Server and socketio.AsyncServer classes that default to the proper namespace when the namespace argument is not given.

In the case that an event has a handler in a class-based namespace, and also a decorator-based function handler, only the standalone function handler is invoked.

It is important to note that class-based namespaces are singletons. This means that a single instance of a namespace class is used for all clients, and consequently, a namespace instance cannot be used to store client specific information.

๐ŸŽ€ Catch-All Namespaces ๐ŸŽ€

Similarily to catch-all event handlers, a "catch-all" namespace can be used when defining event handlers for any connected namespaces that do not have an explicitly defined event handler. As with catch-all events, '*' is used in place of a namespace:

@sio.on('my_event', namespace='*')
def my_event_any_namespace(namespace, sid, data):
    pass

For these events, the namespace is passed as first argument, followed by the regular arguments of the event.

A catch-all class-based namespace handler can be defined by passing '*' as the namespace during registration:

sio.register_namespace(MyCustomNamespace('*'))

A "catch-all" handler for all events on all namespaces can be defined as follows:

@sio.on('*', namespace='*')
def any_event_any_namespace(event, namespace, sid, data):
    pass

Event handlers with catch-all events and namespaces receive the event name and the namespace as first and second arguments.


๐Ÿ’– User Sessions ๐Ÿ’–

The server can maintain application-specific information in a user session dedicated to each connected client. Applications can use the user session to write any details about the user that need to be preserved throughout the life of the connection, such as usernames or user ids.

The save_session() and get_session() methods are used to store and retrieve information in the user session:

@sio.event
def connect(sid, environ):
    username = authenticate_user(environ)
    sio.save_session(sid, {'username': username})

@sio.event
def message(sid, data):
    session = sio.get_session(sid)
    print('message from ', session['username'])

For the asyncio server, these methods are coroutines:

@sio.event
async def connect(sid, environ):
    username = authenticate_user(environ)
    await sio.save_session(sid, {'username': username})

@sio.event
async def message(sid, data):
    session = await sio.get_session(sid)
    print('message from ', session['username'])

The session can also be manipulated with the session() context manager:

@sio.event
def connect(sid, environ):
    username = authenticate_user(environ)
    with sio.session(sid) as session:
        session['username'] = username

@sio.event
def message(sid, data):
    with sio.session(sid) as session:
        print('message from ', session['username'])

For the asyncio server, an asynchronous context manager is used:

@sio.event
async def connect(sid, environ):
    username = authenticate_user(environ)
    async with sio.session(sid) as session:
        session['username'] = username

@sio.event
async def message(sid, data):
    async with sio.session(sid) as session:
        print('message from ', session['username'])

The get_session(), save_session() and session() methods take an optional namespace argument. If this argument isn't provided, the session is attached to the default namespace.

Note: the contents of the user session are destroyed when the client disconnects. In particular, user session contents are not preserved when a client reconnects after an unexpected disconnection from the server. no cap, it's gone gone ๐Ÿ˜ญ

๐ŸŒˆ Cross-Origin Controls ๐ŸŒˆ

For security reasons, this server enforces a same-origin policy by default. In practical terms, this means the following:

  • If an incoming HTTP or WebSocket request includes the Origin header, this header must match the scheme and host of the connection URL. In case of a mismatch, a 400 status code response is returned and the connection is rejected.
  • No restrictions are imposed on incoming requests that do not include the Origin header.

If necessary, the cors_allowed_origins option can be used to allow other origins. This argument can be set to a string to set a single allowed origin, or to a list to allow multiple origins. A special value of '*' can be used to instruct the server to allow all origins, but this should be done with care, as this could make the server vulnerable to Cross-Site Request Forgery (CSRF) attacks.

bestie please be careful with '*' โ€” it's giving open door energy and CSRF attacks are NOT the vibe ๐Ÿ’€

๐Ÿ‘‘ Monitoring and Administration ๐Ÿ‘‘

The Socket.IO server can be configured to accept connections from the official Socket.IO Admin UI <https://socket.io/docs/v4/admin-ui/>. This tool provides real-time information about currently connected clients, rooms in use and events being emitted. It also allows an administrator to manually emit events, change room assignments and disconnect clients. The hosted version of this tool is available at <https://admin.socket.io>.

Given that enabling this feature can affect the performance of the server, it is disabled by default. To enable it, call the instrument() method. For example:

import os
import socketio

sio = socketio.Server(cors_allowed_origins=[
    'http://localhost:5000',
    'https://admin.socket.io',
])
sio.instrument(auth={
    'username': 'admin',
    'password': os.environ['ADMIN_PASSWORD'],
})

This configures the server to accept connections from the hosted Admin UI client. Administrators can then open <https://admin.socket.io> in their web browsers and log in with username admin and the password given by the ADMIN_PASSWORD environment variable. To ensure the Admin UI front end is allowed to connect, CORS is also configured.

Consult the reference documentation to learn about additional configuration options that are available.


๐Ÿ” Debugging and Troubleshooting ๐Ÿ”

To help you debug issues, the server can be configured to output logs to the terminal:

import socketio

# standard Python
sio = socketio.Server(logger=True, engineio_logger=True)

# asyncio
sio = socketio.AsyncServer(logger=True, engineio_logger=True)

The logger argument controls logging related to the Socket.IO protocol, while engineio_logger controls logs that originate in the low-level Engine.IO transport. The value given to these arguments controls logging behavior:

  • True: Enables log output to stderr at the INFO level.
  • False: Enables log output to stderr at the ERROR level. This is the default.
  • A logging.Logger instance: Uses the provided logger without additional configuration.

Logging can help identify the cause of connection problems, 400 responses, bad performance and other issues.

fr fr, when something's broken the logs are your bestie ๐Ÿซถ


โšก Concurrency and Web Server Integration โšก

The Socket.IO server can be configured with different concurrency models depending on the needs of the application and the web server that is used. The concurrency model is given by the async_mode argument in the server. For example:

sio = socketio.Server(async_mode='threading')

okay bestie so now that your server object exists, the next thing to figure out is how it actually gets its concurrency on ๐Ÿซถ the bits below walk through every concurrency option you've got for both the synchronous servers and the async ones. pick your fighter ๐Ÿ’…


โœจ Standard Modes โœจ

  • threading โ€” the server uses Python threads for concurrency and will happily run on any multi-threaded WSGI server. this is the default mode when no other concurrency libraries are installed. lowkey the no-thoughts-just-vibes option ๐ŸŽ€
  • gevent โ€” the server uses greenlets via the gevent library (http://www.gevent.org/) for concurrency. you're gonna need a web server that's compatible with gevent. ๐ŸŒฑ
  • gevent_uwsgi โ€” a lil variation on gevent mode that's designed to play nice with the uWSGI web server (https://uwsgi-docs.readthedocs.io/en/latest/). ๐Ÿฆ‹
  • eventlet โ€” the server uses greenlets via the eventlet library (http://eventlet.net/) for concurrency. you need a web server compatible with eventlet. heads up tho: using eventlet is NOT recommended, because the project is in maintenance mode. ๐Ÿ™ˆ

๐ŸŒˆ Asyncio Modes ๐ŸŒˆ

all the async options are based on the asyncio package (https://docs.python.org/3/library/asyncio.html) from the Python standard library, with teeny variations depending on which web server platform you're on. โญ


๐Ÿš€ Deployment Strategies ๐Ÿš€

the sections below are basically a tour of all the ways you can deploy Socket.IO servers. take notes, bestie ๐Ÿ’œ

๐Ÿ’– Gunicorn ๐Ÿ’–

the simplest deployment strategy for the Socket.IO server is the wildly popular Gunicorn (http://gunicorn.org) web server, run in multi-threaded mode. you've gotta wrap the Socket.IO server in the socketio.WSGIApp class so it's compatible with the WSGI protocol:

sio = socketio.Server(async_mode='threading')
app = socketio.WSGIApp(sio)

if you want, socketio.WSGIApp can also forward any traffic that is not Socket.IO over to another WSGI application โ€” which means you can deploy a standard WSGI web app built with frameworks like Flask or Django and the Socket.IO server as one cute little bundle ๐ŸŽ€:

sio = socketio.Server(async_mode='threading')
app = socketio.WSGIApp(sio, other_wsgi_app)

the example below shows how to start a Socket.IO application using Gunicorn's threaded worker class:

$ gunicorn --workers 1 --threads 100 --bind 127.0.0.1:5000 module:app

with that config, the server will be able to handle close to 100 concurrent clients. slay ๐Ÿ”ฅ

you can also use more than one worker process, but that comes with two extra requirements:

  • The clients must connect directly over WebSocket. The long-polling transport is incompatible with the way Gunicorn load balances requests among workers. To disable long-polling in the server, add transports=['websocket'] in the server constructor. Clients will have a similar option to initiate the connection with WebSocket.
  • The socketio.Server() instances in each worker must be configured with a message queue to allow the workers to communicate with each other. See the Using a Message Queue section for more information.

when you're running multiple workers, the approximate number of connections the server can accept is calculated as the number of workers multiplied by the number of threads per worker. math girlies rise ๐Ÿงฎโœจ

and ngl, Gunicorn can also be used alongside uvicorn, gevent and eventlet. those options get discussed in their own sections below. ๐Ÿ’…

๐ŸŒธ Uvicorn (and other ASGI web servers) ๐ŸŒธ

when you're working with an asynchronous Socket.IO server, the easiest deployment strategy is an ASGI web server like Uvicorn (https://www.uvicorn.org/).

the socketio.ASGIApp class is an ASGI-compatible application that can forward Socket.IO traffic to a socketio.AsyncServer instance:

sio = socketio.AsyncServer(async_mode='asgi')
app = socketio.ASGIApp(sio)

if you want, socketio.ASGIApp can forward any traffic that is not Socket.IO to another ASGI application, which makes it possible to deploy a standard ASGI web app built with a framework like FastAPI and the Socket.IO server as a bundle ๐Ÿซถ:

sio = socketio.AsyncServer(async_mode='asgi')
app = socketio.ASGIApp(sio, other_asgi_app)

the following example starts the application with Uvicorn:

uvicorn --port 5000 module:app

Uvicorn can also be used through its Gunicorn worker:

gunicorn --workers 1 --worker-class uvicorn.workers.UvicornWorker --bind 127.0.0.1:5000

see the Gunicorn section above for info on how to use Gunicorn with multiple workers. ๐Ÿ‘‘

๐Ÿฆ‹ Hypercorn, Daphne, and other ASGI servers ๐Ÿฆ‹

to use an ASGI web server other than Uvicorn, configure the application for ASGI exactly like we showed above for Uvicorn, then follow the docs of whatever web server you picked to start the application. literally just that ๐Ÿ’œ

๐Ÿ“ Aiohttp ๐Ÿ“

another option for deploying an asynchronous Socket.IO server is the Aiohttp (http://aiohttp.readthedocs.io/) web framework and server. instances of the socketio.AsyncServer class will automatically use Aiohttp if the library is installed. if you wanna demand it explicitly, pass the async_mode option in the constructor:

sio = socketio.AsyncServer(async_mode='aiohttp')

a server configured for Aiohttp must be attached to an existing application:

app = web.Application()
sio.attach(app)

the Aiohttp application can define regular routes that coexist with the Socket.IO server. a typical pattern is adding routes that serve a client application and any associated static files. โœจ

the Aiohttp application is then executed in the usual manner:

if __name__ == '__main__':
    web.run_app(app)

๐Ÿ’š Gevent ๐Ÿ’š

when a multi-threaded web server just can't satisfy the concurrency and scalability needs of your application, one option to try is Gevent (http://www.gevent.org). Gevent is a coroutine-based concurrency library built on greenlets, which are significantly lighter than threads. ๐Ÿชถ

instances of the socketio.Server class will automatically use Gevent if the library is installed. to request gevent explicitly, pass the async_mode option in the constructor:

sio = socketio.Server(async_mode='gevent')

the Socket.IO server must be wrapped by the socketio.WSGIApp class so it's compatible with the WSGI protocol:

app = socketio.WSGIApp(sio)

if you want, socketio.WSGIApp can forward any traffic that is not Socket.IO to another WSGI application, which means you can deploy a standard WSGI web app built with frameworks like Flask or Django and the Socket.IO server as a bundle:

sio = socketio.Server(async_mode='gevent')
app = socketio.WSGIApp(sio, other_wsgi_app)

a server configured for Gevent gets deployed as a regular WSGI application using the socketio.WSGIApp you just made:

from gevent import pywsgi

pywsgi.WSGIServer(('', 8000), app).serve_forever()

๐ŸŒฑ Gevent with Gunicorn ๐ŸŒฑ

an alternative to running the gevent WSGI server like above is to use Gunicorn (gunicorn.org) with its Gevent worker. the command to launch the application under Gunicorn and Gevent is right here:

$ gunicorn -k gevent -w 1 -b 127.0.0.1:5000 module:app

see the Gunicorn section above for info on using Gunicorn with multiple workers. ๐Ÿ’…

Gevent provides a monkey_patch() function that replaces all the blocking functions in the standard library with equivalent asynchronous versions. while the Socket.IO server does not require monkey patching, other libraries โ€” like database or message queue drivers โ€” are likely to need it. ๐Ÿ™ˆ

๐Ÿงš Gevent with uWSGI ๐Ÿงš

when you use the uWSGI server together with gevent, the Socket.IO server can take advantage of uWSGI's native WebSocket support. ๐Ÿ”ฅ

instances of the socketio.Server class will automatically use this option for asynchronous operations if both gevent and uWSGI are installed and eventlet is not installed. to request this async mode explicitly, pass the async_mode option in the constructor:

# gevent with uWSGI
sio = socketio.Server(async_mode='gevent_uwsgi')

a complete explanation of configuring and using the uWSGI server is beyond the scope of this documentation, bestie. the uWSGI server is a pretty complex package with a large and comprehensive set of options. it must be compiled with WebSocket and SSL support for the WebSocket transport to be available. as a lil introduction, the following command starts a uWSGI server for the latency.py example on port 5000:

$ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app

๐ŸŒช๏ธ Tornado ๐ŸŒช๏ธ

instances of the socketio.AsyncServer class will automatically use Tornado (http://www.tornadoweb.org//) if the library is installed. to request it explicitly, pass the async_mode option in the constructor:

sio = socketio.AsyncServer(async_mode='tornado')

a server configured for Tornado must include a request handler for Socket.IO:

app = tornado.web.Application(
    [
        (r"/socket.io/", socketio.get_tornado_handler(sio)),
    ],
    # ... other application options
)

the Tornado application can define other routes that coexist with the Socket.IO server. a typical pattern is adding routes that serve a client application and any associated static files. ๐ŸŽ€

the Tornado application is then executed in the usual manner:

app.listen(port)
tornado.ioloop.IOLoop.current().start()

๐Ÿ’€ Eventlet ๐Ÿ’€

Note: Eventlet is not in active development anymore, and for that reason the current recommendation is to not use it for new projects.

Eventlet (http://eventlet.net/) is a high performance concurrent networking library for Python that uses coroutines, letting you write code in the same style you'd use with the blocking standard library functions. a Socket.IO server deployed with eventlet has access to the long-polling and WebSocket transports. โœจ

instances of the socketio.Server class will automatically use eventlet for asynchronous operations if the library is installed. to request its use explicitly, pass the async_mode option in the constructor:

sio = socketio.Server(async_mode='eventlet')

a server configured for eventlet gets deployed as a regular WSGI application using the socketio.WSGIApp you built:

import eventlet

app = socketio.WSGIApp(sio)
eventlet.wsgi.server(eventlet.listen(('', 8000)), app)

๐Ÿ Eventlet with Gunicorn (the โœจfancyโœจ alternative)

So like, instead of running the eventlet WSGI server the way we just talked about, you could use gunicorn (that's <gunicorn.org> bestie), which is a fully featured pure Python web server. The command to launch the application under gunicorn is literally this and nothing else:

$ gunicorn -k eventlet -w 1 module:app

And if you wanna know how to use Gunicorn with multiple workers, go peek at the Gunicorn section above, it's all there ๐Ÿ’…

Now โ€” eventlet gives you a monkey_patch() function that swaps out all the blocking functions in the standard library for equivalent asynchronous versions. And like, python-socketio does not require monkey patching, no cap, but other libraries โ€” database drivers especially โ€” are probably gonna need it. So keep that in your back pocket ๐Ÿซถ

๐ŸŒธ Sanic

Note: the Sanic integration has not been updated in a long time. It is currently recommended that a Sanic application is deployed with the ASGI integration.

Like... it's giving "left on read" energy, ngl. ASGI is the move now โœจ


๐Ÿ“ฌ Using a Message Queue

Okay so when you're working with distributed applications, you're often gonna need to access Socket.IO functionality from multiple processes. There are two specific use cases and they're both extremely valid:

  • Highly available applications may want to use horizontal scaling of the Socket.IO server so they can handle a very large number of concurrent clients. Slay, honestly.
  • Applications that use work queues such as <http://www.celeryproject.org/> Celery may need to emit an event to a client once a background job completes. And the most convenient place to carry out this task? The worker process that handled this job. Periodt.

So as a solution to the above problems, the Socket.IO server can be configured to connect to a message queue such as <http://redis.io/> Redis or <https://www.rabbitmq.com/> RabbitMQ, so it can communicate with other related Socket.IO servers or auxiliary workers. It's giving group chat but for servers ๐Ÿ’–

๐Ÿ“ Redis

To use a Redis message queue, a Python Redis client must be installed:

# socketio.Server class
pip install redis

The Redis queue is configured through the socketio.RedisManager and socketio.AsyncRedisManager classes. These classes connect directly to the Redis store and use the queue's pub/sub functionality:

# socketio.Server class
mgr = socketio.RedisManager('redis://')
sio = socketio.Server(client_manager=mgr)

# socketio.AsyncServer class
mgr = socketio.AsyncRedisManager('redis://')
sio = socketio.AsyncServer(client_manager=mgr)

And the client_manager argument is the โœจmagicโœจ โ€” it instructs the server to connect to the given message queue, and to coordinate with other processes connected to that same queue. Bestie behavior fr fr ๐Ÿ‘‘

๐Ÿงš Kombu

<http://kombu.readthedocs.org/en/latest/> Kombu is a Python package that gives you access to RabbitMQ and like, many other message queues. Install it with pip:

pip install kombu

To use RabbitMQ or other AMQP protocol compatible queues, that's the only required dependency. Iconic. But for other message queues, Kombu may need additional packages โ€” for example, to use a Redis queue via Kombu, the Python package for Redis needs to be installed as well:

pip install redis

The queue is configured through the socketio.KombuManager:

mgr = socketio.KombuManager('amqp://')
sio = socketio.Server(client_manager=mgr)

The connection URL you pass to the KombuManager constructor is passed directly to Kombu's <http://kombu.readthedocs.org/en/latest/userguide/connections.html> Connection object, so go read the Kombu documentation if you need to know how to build the correct URL for a given message queue. Do your research, it's very main character of you ๐Ÿ”ฅ

Note: Kombu currently does not support asyncio, so it cannot be used with the socketio.AsyncServer class. Sorry bestie, it's just not that girl ๐Ÿ’€

๐Ÿ’œ Kafka

Apache <https://kafka.apache.org/> Kafka is supported through the <https://kafka-python.readthedocs.io/en/master/index.html> kafka-python package:

pip install kafka-python

Access to Kafka is configured through the socketio.KafkaManager class:

mgr = socketio.KafkaManager('kafka://')
sio = socketio.Server(client_manager=mgr)
Note: Kafka currently does not support asyncio, so it cannot be used with the socketio.AsyncServer class. Same energy as Kombu, same heartbreak ๐Ÿ˜ญ

๐Ÿฆ‹ AioPika

A RabbitMQ message queue is supported in asyncio applications through the <https://aio-pika.readthedocs.io/en/latest/> AioPika package. You need to install aio_pika with pip:

pip install aio_pika

The RabbitMQ queue is configured through the socketio.AsyncAioPikaManager class:

mgr = socketio.AsyncAioPikaManager('amqp://')
sio = socketio.AsyncServer(client_manager=mgr)

See? Async girlies get their RabbitMQ too ๐ŸŒˆ

๐Ÿ”’ Deploying the Message Queue for Production

Okay listen up, because for a production deployment there are a few recommendations to keep your application secure and we're not skipping them:

First of all, the message queue should never be listening on a public network interface, so you can make sure external clients never connect to it. For a single node deployment, the queue should only listen on localhost. For a multi-node system, the use of a private network (VPC) โ€” where the communication between servers can happen privately โ€” is highly recommended. Protect your peace and your packets ๐Ÿ’…

In addition, all message queues support authentication and encryption, which can strengthen the security of the deployment. Authentication makes sure that only the Socket.IO servers and related processes have access, while encryption prevents data from being collected by a third-party that is listening on the network. And access credentials can be included in the connection URLs that are passed to the client managers. Cute, convenient, secure โœจ

๐Ÿ“ˆ Horizontal Scaling

Socket.IO is a stateful protocol, which makes horizontal scaling more difficult. Ugh, we know. When deploying a cluster of Socket.IO processes, all processes must connect to the message queue by passing the client_manager argument to the server instance. This enables the workers to communicate and coordinate complex operations such as broadcasts. Teamwork makes the dream work ๐Ÿซถ

And if the long-polling transport is used, then there are two additional requirements that must be met:

  • Each Socket.IO process must be able to handle multiple requests concurrently. This is needed because long-polling clients send two requests in parallel. Worker processes that can only handle one request at a time are not supported.
  • The load balancer must be configured to always forward requests from a client to the same worker process, so that all requests coming from a client are handled by the same node. Load balancers call this sticky sessions, or session affinity.

Two rules, no exceptions, iykyk ๐Ÿ”ฅ

๐Ÿ“ฃ Emitting from external processes

So if you want a process other than a server to connect to the queue and emit a message, the same client manager classes can be used as standalone objects. In this case, the write_only argument should be set to True to disable the creation of a listening thread โ€” which only makes sense in a server anyway. For example:

# connect to the redis queue as an external process
external_sio = socketio.RedisManager('redis://', write_only=True)

# emit an event
external_sio.emit('my event', data={'foo': 'bar'}, room='my room')

And there's a limitation bestie, so listen: a write-only client manager object cannot receive callbacks when emitting. When the external process needs to receive callbacks, using a client to connect to the server with read and write support is a better option than a write-only client manager. Choose wisely ๐ŸŽ€


๐ŸŒˆ API REFERENCE

  • Index
  • Module Index
  • Search Page

๐Ÿ‘‘ Author

Miguel Grinberg

2018, Miguel Grinberg


And that's the whole page, bestie!! We did it, we actually did it ๐ŸŽ‰๐Ÿ’–โœจ This lil' manual is from Dec 29, 2025 and it's page PYTHON-SOCKETIO(1) โ€” go forth and socket, you absolute icon ๐Ÿฆ‹๐Ÿ‘‘๐Ÿ’…

๐Ÿ“ฆ where this page comes from