This release is a pre-release and may not be stable for production use.
python-peerjs-server
An asyncio-based Python port of the PeerJS signalling server, the WebSocket
broker that PeerJS clients use to discover each other and exchange WebRTC offers/answers/candidates.
It's feature-complete and wire-compatible with the original: WebSocket session handling, reconnect, and the HTTP API are all implemented, plus a few deployment conveniences, like rate limiting, proxied-IP support, and bounded queues.
Features
- WebSocket signalling at
{path}/peerjswithid/token/keyquery-param auth - Reconnect support (same
id+tokenre-attaches the socket and drains queued messages) - Per-peer message queueing for offline destinations, with periodic
EXPIREsweeps - Periodic dead-connection reaping based on heartbeat pings
- HTTP API:
GET {path}{key}/id(fresh peer id) andGET {path}{key}/peers(peer list, opt-in viaallow_discovery) - CORS header support
- Fixed-window per-IP rate limiting on the HTTP API
- Optional
X-Forwarded-Fortrust for real client IPs when running behind a reverse proxy
Requirements
- Python 3.13+
websockets>=14(the only runtime dependency, installed automatically below)
Installation
pip install python-peerjs-server
For local development (editable install with test/lint tooling):
pip install -e ".[dev]"
Usage
Run as a console script:
peerjs-server --port 9000 --key peerjs
or as a module:
python -m python_peerjs_server --port 9000 --key peerjs
CLI options
| Flag | Env var | Default | Description |
|---|---|---|---|
--host |
PEERJS_HOST |
:: |
Bind host |
--port |
PEERJS_PORT |
9000 |
Bind port |
--key |
PEERJS_KEY |
peerjs |
Access key clients must supply |
--path |
PEERJS_PATH |
/ |
Path prefix for HTTP/WS routes |
--allow-discovery |
PEERJS_ALLOW_DISCOVERY |
off | Expose GET /peers |
--cors-origin |
PEERJS_CORS_ORIGIN |
unset | CORS origin for HTTP API responses |
--log-level |
PEERJS_LOG_LEVEL |
INFO |
Logging level |
Other server behaviour (timeouts, rate limits, queue caps, proxy trust) is configured via
python_peerjs_server.config.Config when embedding the server programmatically; see
src/python_peerjs_server/config.py for the full set of fields and their
defaults.
Embedding in your own asyncio app
import asyncio
from python_peerjs_server.config import Config
from python_peerjs_server.main import peerjs_serve
asyncio.run(peerjs_serve(Config(port=9000, key="peerjs")))
Logging
If you're running via the CLI, --log-level/PEERJS_LOG_LEVEL already configures this for you (it calls
logging.basicConfig); the rest of this section is for embedding python_peerjs_server in your own app.
Every module logs under the python_peerjs_server namespace (logging.getLogger(__name__)), so that one logger name
is the knob to turn:
import logging
# See python_peerjs_server logs at INFO+ on stderr
logging.getLogger("python_peerjs_server").setLevel(logging.INFO)
logging.getLogger("python_peerjs_server").addHandler(logging.StreamHandler())
# Or, if your app already configured the root logger, just set the level;
# propagation does the rest
logging.getLogger("python_peerjs_server").setLevel(logging.DEBUG)
# Silence a noisy sub-logger specifically
logging.getLogger("python_peerjs_server.services.web_socket_session").setLevel(logging.WARNING)
A NullHandler is attached by default, so the library stays silent unless logging is configured (either on
python_peerjs_server
or higher up).
Connecting a PeerJS client
This server only implements the signalling protocol; clients connect using the PeerJS client library (docs, GitHub):
const peer = new Peer("some-id", {
host: "localhost",
port: 9000,
path: "/",
key: "peerjs",
});
Runnable versions of the standalone and FastAPI embedding patterns live in examples/:
standalone.py and fastapi_app.py.
CORS
The PeerJS client calls the HTTP API (GET {path}{key}/id) before it opens the WebSocket. If your page is served
from a different origin than the broker (e.g. a Vite/CRA dev server on localhost:5173 talking to a broker on
localhost:9000), the browser will block that request with a CORS error unless you set --cors-origin /
PEERJS_CORS_ORIGIN / Config.cors_origin to the exact origin (scheme + host + port) serving the page:
peerjs-server --port 9000 --key peerjs --cors-origin http://localhost:5173
It's unset by default (no Access-Control-Allow-Origin header sent), which is fine for same-origin deployments.
Running alongside another web framework
peerjs_serve() is just a coroutine; you can run it inside whatever process already hosts your app instead of standing
up a separate service. Pass handle_signals=False so it doesn't fight your app for SIGINT/SIGTERM handling, and
pick a port distinct from your main app's.
Runnable versions of both patterns below live in examples/: tornado_app.py
and flask_app.py.
Tornado
Tornado runs on the standard asyncio event loop, so the broker can simply be scheduled as another task on it:
import asyncio
import tornado.ioloop
import tornado.web
from python_peerjs_server.config import Config
from python_peerjs_server.main import peerjs_serve
class MainHandler(tornado.web.RequestHandler):
def get(self) -> None:
self.write("hello from tornado")
async def main() -> None:
app = tornado.web.Application([(r"/", MainHandler)])
app.listen(8888)
asyncio.create_task(peerjs_serve(Config(port=9000, key="peerjs"), handle_signals=False))
await asyncio.Event().wait() # run forever
if __name__ == "__main__":
asyncio.run(main())
Flask
Flask's dev server is sync/WSGI and blocks its own thread, so the broker has to run on an event loop in a separate thread:
import asyncio
import threading
from flask import Flask
from python_peerjs_server.config import Config
from python_peerjs_server.main import peerjs_serve
app = Flask(__name__)
@app.get("/")
def index() -> str:
return "hello from flask"
def run_python_peerjs_server() -> None:
asyncio.run(peerjs_serve(Config(port=9000, key="peerjs"), handle_signals=False))
if __name__ == "__main__":
threading.Thread(target=run_python_peerjs_server, daemon=True).start()
app.run(port=8888)
For an ASGI app (FastAPI, Starlette) you can instead start it from a lifespan/startup hook with asyncio.create_task,
the same way as the Tornado example: both share a loop with no extra thread needed.
Development
pip install -e ".[dev]" # install with dev/test/lint dependencies
pytest # run tests
ruff check src # lint
ruff format src # format
mypy src # type check
bandit -r src # security scan
License
MIT, see LICENSE.
Release files for python-peerjs-server 1.0.0b2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| python_peerjs_server-1.0.0b2.tar.gz | 24.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| python_peerjs_server-1.0.0b2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 44.8 kB
Release files / python_peerjs_server-1.0.0b2.tar.gz
| Download URL | python_peerjs_server-1.0.0b2.tar.gz |
|---|---|
| Size | 24.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3b9930db0443c509f917389e37c6e7634efc6c9f31e14a2272c110f41b62f9dd
|
|
BLAKE2b-256 checksum How to use checksums |
26d5947adc6814dda682261e597dca1b674057b0c0e6d99752a775f250bd5009
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.5
|
Release files / python_peerjs_server-1.0.0b2-py3-none-any.whl
| Download URL | python_peerjs_server-1.0.0b2-py3-none-any.whl |
|---|---|
| Size | 20.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3fcb8c2f5ba583b7e8cccd6ee8144f4e5dd37313096e2926a4da20e068dd0a65
|
|
BLAKE2b-256 checksum How to use checksums |
2c30b73646f6747ec2e1a63fb8b7206af096d9dacf3b4a71f6946ce550095dfe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.5
|