# How to run Jupyter notebooks headless in the cloud Upload the notebook to a sandbox, run it with papermill or `jupyter nbconvert --execute`, and 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 under a cent a run, and a sandbox is ready for its first Python command in 351 ms at the median, measured on 24 September 2026 ([speed](/docs/speed)). 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 and write a new notebook with every output in it, which is what a scheduled job or CI wants. The interpreter suits code that decides the next cell after seeing the last one, such as an agent. ## Build an image with Jupyter The default image has Python 3.12 with pandas 2.1.4, NumPy 1.26.4 and matplotlib 3.6.3. Add the notebook tools and whatever 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 costs nothing, and the image starts every run with those packages already installed ([custom images](/docs/images)). ## Run a parameterised notebook with papermill papermill reads the cell tagged `parameters` and inserts a new cell tagged `injected-parameters` right after it, holding the values you pass with `-p` (papermill's documentation, read 25 September 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 a cell raised. Keep the tail of `stderr` for the alert, and the output notebook for whoever fixes it. ## Or run it as it is with nbconvert ```bash no-run jupyter nbconvert --to notebook --execute --ExecutePreprocessor.timeout=600 analysis.ipynb ``` nbconvert gives each cell 30 seconds by default and stops at the first error; `--ExecutePreprocessor.timeout` raises the limit, and `allow_errors` runs the notebook to the end regardless (nbconvert's documentation, read 25 September 2026). Put the command in an `exec` exactly as in the papermill example. ## Many parameter sets at once A report per region, per customer or per model checkpoint is the same notebook with different parameters. Give each its own sandbox and run them together; a paid account starts with room for 100 at once, and a create beyond that 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** go up with the notebook in `files.upload`. - **A bucket** mounts as a directory, with the access key held by the host's proxy rather than 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, and the proxy swaps in 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; `extend` or `keepAlive` moves the lease | | 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 bake it in | | Someone else's notebook | A microVM of its own; network narrowed or 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: 900 runs a month. Each keeps a 2 vCPU, 4 GiB sandbox for 10 minutes, and 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)). Between runs nothing is running, so nothing is billed. 50 sandbox hours come free on a new account, 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 25 September 2026. - [nbconvert: executing notebooks](https://nbconvert.readthedocs.io/en/latest/execute_api.html): `--execute`, the 30-second cell timeout and `allow_errors`, read 25 September 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 25 September 2026.