Skip to content
Guides · Use cases

Build a Stateful Code Interpreter with a Jupyter Kernel

Updated October 27, 2026 .md

Short answer: Install ipykernel and jupyter_client in a venv inside a Sandboxer sandbox, start the kernel with sbx exec --detach, and send each cell through a small client script. Variables persist between calls. In our test the same kernel and its variables survived sbx stop and sbx resume; a cold boot loses them, so check the kernel after resuming.

What you build#

A code interpreter tool for an LLM is a long-lived Python process that runs one snippet at a time and keeps its namespace. A Jupyter kernel is that process, and the Jupyter messaging protocol gives you stdout, results and tracebacks for each cell. You run the kernel inside the sandbox, so the model's code never runs on your machine.

Piece Where it runs What it does
ipykernel Sandbox, detached Holds the variables, imports and open files
~/k.json Sandbox Connection file: ports and the signing key. Created mode 0600
kexec.py Sandbox, one run per cell Sends one cell, prints its output and status
sbx exec Your app Runs kexec.py with the model's code

Jupyter isn't preinstalled. The base image has Python 3.12 and uv.

1. Install the kernel#

sbx new --name interpreter --type small
id=sb_ugjwy66ac2    # your sandbox id from `sbx new`
sbx exec $id "python3 -m venv ~/jk && ~/jk/bin/pip install -q ipykernel jupyter_client && \
  ~/jk/bin/python -c 'import ipykernel, jupyter_client; print(ipykernel.__version__, jupyter_client.__version__)'"
7.4.0 8.10.0

That took 13 seconds on a small sandbox (single run, October 27, 2026). The venv keeps the kernel's packages apart from the system Python.

2. The client script#

kexec.py connects to the running kernel, executes one cell and prints what a notebook would show. Save it on your machine:

import os, sys
from jupyter_client import BlockingKernelClient

kc = BlockingKernelClient(connection_file=os.path.expanduser("~/k.json"))
kc.load_connection_file()
kc.start_channels()
kc.wait_for_ready(timeout=10)

def show(msg):
    c = msg["content"]
    t = msg["msg_type"]
    if t == "stream":
        sys.stdout.write(c["text"])
    elif t in ("execute_result", "display_data"):
        print(c["data"].get("text/plain", ""))
    elif t == "error":
        print(f"{c['ename']}: {c['evalue']}")

reply = kc.execute_interactive(sys.argv[1], output_hook=show, timeout=60)
print(f"[{reply['content']['status']} #{reply['content']['execution_count']}]")

The last line prints ok or error and the kernel's execution counter, which tells your app whether the cell failed and whether it is still talking to the same kernel.

3. Start the kernel and run cells#

sbx scp ./kexec.py $id:/home/user/
sbx exec $id --detach "~/jk/bin/python -m ipykernel_launcher -f ~/k.json"   # "pid":578

run() { sbx exec $id "~/jk/bin/python kexec.py \"$1\""; }
run "x = 41"
run "print(x + 1)"
run "import pandas as pd"
run "import os, time; pid = os.getpid(); started = time.time(); data = [i * i for i in range(10)]; pid"
run "sum(data)"
run "1/0"
run "x"
[ok #1]
42
[ok #2]
ModuleNotFoundError: No module named 'pandas'
[error #3]
578
[ok #4]
285
[ok #5]
ZeroDivisionError: division by zero
[error #6]
41
[ok #7]

Errors don't reset anything: after the ZeroDivisionError, x is still 41. To add a package mid-session, install it into the venv and import it in the next cell. The running kernel picks it up without a restart:

sbx exec $id "~/jk/bin/pip install -q humanize && ~/jk/bin/python kexec.py \"import humanize; humanize.naturalsize(10**9)\""
'1.0 GB'
[ok #8]

Per-call latency#

In our single test run (October 27, 2026), each run call took 0.63 to 0.99 seconds from our machine. An empty sbx exec $id "true" took 0.25 seconds, and kexec.py alone took 0.64 seconds inside the sandbox, mostly Python start-up and connecting to the kernel. If that matters, keep one client process connected in the sandbox and feed it cells, instead of starting kexec.py per cell.

4. Stop, resume and check the kernel#

sbx stop $id --wait     # returns once the sandbox is archived
sbx resume $id          # 1.38 s to ready (single run, October 27, 2026)
run "x"
run "print(x + 1)"
run "sum(data), pid, round(time.time() - started)"
sbx exec $id --status 578 | jq -c '{status,running}'
41
[ok #10]
42
[ok #11]
(285, 578, 193)
[ok #12]
{"status":"running","running":true}

The memory was restored in this run. It was the same kernel process (PID 578), its counter carried on from #9 to #10, and x, data and the imports were still there. We ran a second stop and sbx resume $id --require-memory, and x, pid returned (41, 578) again.

What memoryRestored said: null, on both resumes in our run on 2026-10-27, when the field wasn't yet set on a successful restore. Since a fix that shipped later that night, every resume reports memoryRestored: true or false, with a resumeReason (cpu_mismatch, resized, no_memory_snapshot or restore_failed) when it's false. Checking the kernel's process and execution counter, as above, still works either way. Memory is restored only on a machine with the same CPU type: if none is free within 2 minutes, the resume boots from disk on a different one (see snapshots). The files are kept, but processes are lost, so your kernel and its variables are gone. Pass --require-memory if you'd rather wait (up to 30 minutes) than boot from disk.

When the kernel doesn't survive#

We forced a cold boot by resuming at a new size (sbx resume $id --type default). In our run the memoryRestored field was null here too (it now reports false with resumeReason: "resized"), but the kernel was gone:

$ sbx exec $id --status 578 | jq -c '{status,running}'
{"status":"lost","running":false}
$ sbx exec $id "~/jk/bin/python kexec.py x"
RuntimeError: Kernel died before replying to kernel_info

~/k.json, kexec.py and the venv were all still on disk. So don't rely on the field alone: after every resume, run a cheap cell and compare the execution counter. If it starts again at #1, or the call fails, the namespace is empty. Your app then has to rebuild it, for example by replaying the cells it has logged, or by having the model save important results to files.

To have a kernel come back on its own after a cold boot, run it as a systemd service. Enabled services start on boot; hand-started processes don't:

sudo tee /etc/systemd/system/kernel.service >/dev/null <<'UNIT'
[Unit]
Description=Jupyter kernel for the code interpreter
After=network-online.target

[Service]
User=user
WorkingDirectory=/home/user
ExecStart=/home/user/jk/bin/python -m ipykernel_launcher -f /home/user/k.json
Restart=always

[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload && sudo systemctl enable --now kernel.service

After a sudo reboot inside the sandbox, systemctl is-active kernel.service printed active and kexec.py connected with the same ~/k.json. As expected, the namespace was new: y returned NameError at [error #1]. A service brings back the kernel, not its variables. Only a memory-restoring resume keeps those.

FAQ#

Do I need Jupyter Server or Kernel Gateway?#

No. ipykernel and jupyter_client are enough when your app reaches the kernel through sbx exec or the API. A gateway is only useful if you want to reach the kernel over HTTP. If you do, host its port as private, not public, since anyone who reaches a kernel can run code.

Is the kernel's unencrypted TCP warning a problem?#

The kernel logs that it runs over TCP without encryption. ipykernel listens on 127.0.0.1 by default and the client runs in the sandbox too, so the traffic stays inside the VM. Messages are signed with the key in ~/k.json, which is readable only by user.

What does a paused interpreter cost?#

Nothing for compute. Billing is per second while a sandbox runs; a stopped one's disk and memory image count toward your plan's included storage. An idle sandbox also pauses itself with memory after your plan's idle time, and the next sbx exec wakes it (see long-running tasks).

Can I give each user their own interpreter?#

Yes. Create one sandbox per user or per conversation, each with its own kernel. Each sandbox is a separate microVM, so one user's code can't see another's variables or files.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.

Read the guide
Best E2B alternatives in 2026, ranked

If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.

Read the guide