Kernel architecture and the messaging protocol

Jupyter · Expert · 16 min

Understanding the protocol explains kernel behavior (busy/idle, interrupts, stray output) and lets you build tools that run code in kernels.

Jupyter separates the user interface from code execution. A kernel is a standalone process that speaks the Jupyter messaging protocol over ZeroMQ sockets. A connection file (JSON with IP, ports, transport and an HMAC key) tells clients where to connect; every message is signed with that key.

Each kernel exposes several channels:

  • shell: requests and replies, such as execute_request, complete_request (tab completion), inspect_request (Shift+Tab help) and kernel_info_request;
  • iopub: broadcasts to all clients: status (busy/idle), execute_input, stream (stdout/stderr), display_data, execute_result, error;
  • stdin: lets the kernel ask the client for input, as input() does;
  • control: high-priority messages such as shutdown, interrupt and debug requests;
  • heartbeat: a ping to detect a dead kernel.

A message has a header (with msg_id and msg_type), a parent_header (linking replies and outputs to the request that caused them), metadata and content. Front ends route output to the right cell by parent_header.msg_id, which is why output from a background thread can appear under a different cell than you expect.

In the browser, the Jupyter Server bridges ZeroMQ to WebSockets. Any language can provide a kernel by implementing the protocol, directly or with libraries such as xeus.

jupyter_client lets Python start and drive kernels: KernelManager starts the process and BlockingKernelClient sends requests and reads messages. Messages are signed but, over the default TCP transport, not encrypted; recent ipykernel versions warn about this, so keep kernels on localhost or use IPC transport.

Example

from jupyter_client.manager import start_new_kernel

km, kc = start_new_kernel(kernel_name="python3")
try:
    msg_id = kc.execute("import os\nprint('pid', os.getpid() > 0)\n6 * 7")
    while True:
        msg = kc.get_iopub_msg(timeout=10)
        if msg["parent_header"].get("msg_id") != msg_id:
            continue                       # output from another request
        kind, content = msg["msg_type"], msg["content"]
        if kind == "stream":
            print("stream:", content["text"].strip())          # stream: pid True
        elif kind == "execute_result":
            print("result:", content["data"]["text/plain"])    # result: 42
        elif kind == "status" and content["execution_state"] == "idle":
            break                          # request finished
    reply = kc.get_shell_msg(timeout=10)
    print(reply["msg_type"], reply["content"]["status"])  # execute_reply ok
finally:
    kc.stop_channels()
    km.shutdown_kernel()

Remember

  • Kernels speak a JSON messaging protocol over ZeroMQ channels: shell, iopub, stdin, control, heartbeat
  • Outputs are linked to requests through parent_header.msg_id
  • jupyter_client starts kernels and sends requests from Python

Watch out for

  • Assuming outputs arrive on the shell channel; results and prints are broadcast on iopub.

Check yourself

  1. On which channel does a client receive printed output and the busy/idle status of a kernel?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: jupyter-client.readthedocs.io/en/stable/messaging.html

Builds on

Unlocks

Nothing builds on this yet.