# How to run Jupyter notebooks headless in the cloud Upload the notebook to a sandbox. Run it with papermill or `jupyter nbconvert --execute`. Then download the executed copy. **On Runtime, a notebook run pays for the CPU its cells use, and for memory only while it runs.** A nightly report that keeps a 2 vCPU, 4 GiB sandbox for ten minutes costs less than a cent per run. No server sits idle between runs. ## Two ways to run a notebook | Approach | Best for | What you get back | | ------------------------------ | --------------------------------------------- | ------------------------------------------- | | papermill on the `.ipynb` file | Scheduled reports with parameters | An executed notebook, one per parameter set | | `jupyter nbconvert --execute` | Running a notebook as it is | An executed notebook | | Runtime's code interpreter | A program or agent sending one cell at a time | Each cell's text, tables and PNG charts | papermill and nbconvert run the whole file. They write a new notebook that contains every output. A scheduled job or CI pipeline needs this. The interpreter suits code that picks the next cell after it sees the output of the last one, such as an agent. ## Build an image with Jupyter The default image has Python 3.12, pandas 2.1.4, NumPy 1.26.4 and matplotlib 3.6.3. Add the notebook tools and every package your notebooks import: ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); await runtime.images.build({ name: "notebooks", recipe: { pip: ["papermill", "nbconvert", "ipykernel", "scikit-learn"] }, }); ``` Building an image costs nothing. Every run starts with those packages already installed ([custom images](/docs/images)). ## Run a parameterised notebook with papermill papermill reads the cell tagged `parameters`. It inserts a new cell tagged `injected-parameters` right after it. That cell holds the values you pass with `-p` (papermill's documentation, read 26 October 2026). ```ts check import { writeFile } from "node:fs/promises"; import { Runtime } from "withruntime"; const runtime = new Runtime(); await using sbx = await runtime.sandboxes.create({ image: "notebooks", timeoutSeconds: 1800, onLeaseEnd: "stop", labels: { job: "weekly-report" }, }); await sbx.files.upload("./reports", "/workspace/reports"); const run = await sbx.exec( ["papermill", "sales.ipynb", "out/sales-west.ipynb", "-p", "region", "west", "-p", "weeks", "12"], { cwd: "/workspace/reports", timeoutMs: 1_500_000 }, ); if (run.exitCode !== 0) console.error(run.stderr.slice(-2000)); // the failing cell's traceback await writeFile( "sales-west.ipynb", await sbx.files.read("/workspace/reports/out/sales-west.ipynb"), ); ``` ```python check from withruntime import Runtime runtime = Runtime() with runtime.sandboxes.create( image="notebooks", timeout_seconds=1800, on_lease_end="stop", labels={"job": "weekly-report"}, ) as sbx: sbx.files.upload("./reports", "/workspace/reports") run = sbx.exec( ["papermill", "sales.ipynb", "out/sales-west.ipynb", "-p", "region", "west", "-p", "weeks", "12"], cwd="/workspace/reports", timeout_ms=1_500_000, ) if run.exit_code != 0: print(run.stderr[-2000:]) # the failing cell's traceback with open("sales-west.ipynb", "wb") as target: target.write(sbx.files.read("/workspace/reports/out/sales-west.ipynb")) ``` A non-zero exit code means that a cell raised an error. Keep the last part of `stderr` for the alert. Keep the output notebook for the person who fixes the error. ## Or run it as it is with nbconvert ```bash no-run jupyter nbconvert --to notebook --execute --ExecutePreprocessor.timeout=600 analysis.ipynb ``` By default, nbconvert gives each cell 30 seconds and stops at the first error. `--ExecutePreprocessor.timeout` raises the time limit. `allow_errors` runs the notebook to the end even after an error (nbconvert's documentation, read 26 October 2026). Put the command in an `exec` call, as in the papermill example. ## Many parameter sets at once A report for each region, customer or model checkpoint uses the same notebook with different parameters. Give each run its own sandbox, and run them together. A paid account runs 100 sandboxes at the same time. A create request beyond that limit waits for a free slot instead of failing: ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime({ waitForCapacityMs: 600_000 }); const regions = ["west", "east", "north", "south"]; await Promise.all( regions.map(async (region) => { await using sbx = await runtime.sandboxes.create({ image: "notebooks", labels: { region } }); await sbx.files.upload("./reports", "/workspace/reports"); await sbx.exec(["papermill", "sales.ipynb", `out/${region}.ipynb`, "-p", "region", region], { cwd: "/workspace/reports", timeoutMs: 1_500_000, }); await sbx.files.download(`/workspace/reports/out/${region}.ipynb`, `./out/${region}.ipynb`); }), ); ``` ## Data the notebook reads - **Small inputs** are uploaded with the notebook, using `files.upload`. - **A bucket** mounts as a directory. The host's proxy holds the access key, not the sandbox ([mount your own bucket](/docs/storage#mount-your-own-bucket)). - **A warehouse or API key** is stored as a Runtime secret. The notebook reads an environment variable that holds a placeholder. The proxy replaces the placeholder with the real value only on HTTPS requests to the hosts you named ([secrets](/docs/security#secrets-sandboxes-never-see)). ## What notebook jobs need | Need | How Runtime covers it | | ------------------------------ | --------------------------------------------------------------------------------- | | The same environment every run | A versioned image; `notebooks@4` pins one build | | A notebook that runs for hours | Commands up to 24 hours; the sandbox has no time limit | | Charts | Saved in the executed `.ipynb`; the interpreter returns them as PNG | | R code | The interpreter runs R cells; R installs on first use, or is baked into the image | | Someone else's notebook | A microVM of its own; network limited or turned off | | Knowing a run failed | A `sandbox.stopped` webhook, or the exit code in your scheduler | ## What it costs A team runs 30 notebooks every night, which is 900 runs a month. Each run keeps a 2 vCPU, 4 GiB sandbox for 10 minutes. Its cells use 6 CPU-minutes (360 CPU-seconds): ``` CPU: 900 × 360 s / 3,600 × $0.025 = $2.25 Memory: 900 × 600 s / 3,600 × 4 GiB × $0.0075 = $4.50 Total: $6.75 ``` That is 0.75 cents a run, at $0.025 per vCPU-hour of measured CPU and $0.0075 per GiB-hour ([pricing](/docs/pricing)). Nothing runs between runs, so nothing is billed. A new account gets 100 free hours of a 1 GB machine in its first 30 days, with no card needed. ## Sources - [papermill: execute](https://papermill.readthedocs.io/en/latest/usage-execute.html) and [parameterize](https://papermill.readthedocs.io/en/latest/usage-parameterize.html): `-p`, the `parameters` tag and `injected-parameters`, read 26 October 2026. - [nbconvert: executing notebooks](https://nbconvert.readthedocs.io/en/latest/execute_api.html): `--execute`, the 30-second cell timeout and `allow_errors`, read 26 October 2026. Related: [data analysis agent](/use-cases/data-analysis-agent), [what a code interpreter is](/glossary/code-interpreter), [code interpreter for chatbots](/use-cases/code-interpreter-for-chatbots), [Python in a sandbox](/languages/python). Facts on this page were checked on 26 October 2026.