jupygate starts, stops, and restarts Jupyter kernels over a small HTTP API, and muxes each kernel’s zmq channels over one websocket per client. It speaks the standard Jupyter kernels REST shape and the legacy Jupyter websocket message protocol, so existing Jupyter-compatible clients work unchanged. It deliberately has no files API (manage files by running Python in a kernel), no kernelspecs (clients say exactly what to launch), and no HTML.
It is a small, readable alternative to running jupyter_server or kernel_gateway for the one job of hosting kernels. kernel_gateway is a thin skin over jupyter_server’s websocket bridge; jupygate replaces the bridge itself with a simpler architecture:
- One persistent zmq channel set per kernel, owned by the gateway, instead of fresh streams per websocket connection. Clients connect and drop without touching zmq, which deletes jupyter_server’s per-connect “nudge” dance, buffering handoffs, and reconnect races.
- Welcome-based readiness: kernels that implement JEP 65 (ipymini, modern ipykernel) greet each iopub subscription with
iopub_welcome, so readiness is one bounded wait plus onekernel_inforound trip. Kernels without it get jupyter_client-style retries - same loop, it just can’t short-circuit. - Replies route by session: each client signs with its own session id;
parent_header.sessionroutes shell, control, and stdin traffic back to the requester. iopub is broadcast. The kernel’s HMAC key never leaves the gateway - client auth is a bearer token at the HTTP/websocket layer. - Explicit overflow policy: each client has a bounded queue that never drops
status, so a flooded client still sees a truthful busy/idle picture.
One nbdev notebook, core, builds it bottom-up - wire formats (the two websocket encodings), kernel processes, channels (the zmq side and the ready-wait), the mux (routing and fan-out), and the gateway server (the HTTP/websocket surface) - and is source, docs, and tests at once.
pip install jupygatePlus at least one kernel; the examples use ipymini.
Serve a gateway:
jupygate --port 8787 # open, for localhost use
jupygate --port 8787 --token S # every request must carry the tokenThe gateway watches a touch file, ~/.local/state/jupygate/reload/r.py (created at startup): touching it restarts the gateway with new code, killing all kernels - the deliberate restart lever after upgrading jupygate or a kernel package. --reload additionally restarts on package source changes, for development.
Then from any HTTP client:
curl -X POST localhost:8787/api/kernels # -> {"id": "...", ...}
curl localhost:8787/api/kernels # list
curl -X POST localhost:8787/api/kernels/ID/interrupt
curl -X DELETE localhost:8787/api/kernels/IDand connect a websocket to /api/kernels/ID/channels?session_id=... speaking the standard Jupyter message dicts with a channel key (see the wire-formats section of core). Kernel creation accepts explicit launch parameters in the POST body - argv (with {connection_file} placeholder), env, cwd, and username for a sudo/gosu wrap - which is also why a non-localhost deployment should set a token: this API runs arbitrary commands by design, exactly like any Jupyter kernel server.
In Python, the same thing programmatically:
from jupygate.core import create_app, serve
serve(create_app(auth_token='S'), port=8787)