Runtime

How to extract text from PDFs, scanned ones included, in a sandbox

Open each PDF in a throwaway sandbox, pull the embedded text with Poppler or pypdf, and run Tesseract OCR on pages that are only images.

On Runtime OCR pays for the CPU it uses and nothing while it waits. 2,000 PDFs a month with 30 CPU-seconds of extraction and OCR each cost $1.08 on a 2 vCPU, 4 GiB sandbox, at the rates Runtime publishes, read 25 September 2026. Every file is parsed in a Firecracker microVM with its own kernel, so a malformed or malicious PDF from a user never touches your application server.

Two kinds of PDF, two tools

A PDF made by a word processor carries its text; extraction reads it back. A scanned PDF is a picture of a page; it has no text until OCR recognises it. pypdf's documentation says as much: a page that holds only an image gives little or no text, and OCR software such as Tesseract is the tool for it (pypdf docs, read 25 September 2026).

Tool What it does Ubuntu 24.04 package
pdftotext Embedded text to a plain-text file poppler-utils 24.02.0
pdftoppm Each page to a PNG image, for OCR or a vision model poppler-utils 24.02.0
pdfinfo, pdfimages Metadata and page count; the images inside the file poppler-utils 24.02.0
tesseract Text from an image; with pdf, a searchable PDF tesseract-ocr 5.3.4
pypdf Page-by-page text from Python, page.extract_text() pip install pypdf

Versions are Launchpad's for noble, read 25 September 2026.

Build the image once

TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({  name: "pdf",  recipe: { apt: ["poppler-utils", "tesseract-ocr"], pip: ["pypdf"] },});
Pythonfrom withruntime import RuntimeRuntime().images.build(name="pdf", recipe={"apt": ["poppler-utils", "tesseract-ocr"], "pip": ["pypdf"]})

Extract, and fall back to OCR

Try the embedded text first. When it comes back nearly empty, render the pages to images and recognise them.

TypeScriptimport { readFile } from "node:fs/promises";import { Runtime } from "withruntime";const runtime = new Runtime();await using sbx = await runtime.sandboxes.create({  image: "pdf",  network: { internet: false },  timeoutSeconds: 900,  onLeaseEnd: "stop",});await sbx.files.write("/workspace/doc.pdf", await readFile("invoice.pdf"));await sbx.exec(["pdftotext", "doc.pdf", "doc.txt"], { check: true, timeoutMs: 120_000 });let text = await sbx.files.readText("/workspace/doc.txt");if (text.trim().length < 50) {  // A scan: pages to PNG at 300 dpi, then OCR each one.  await sbx.exec("pdftoppm -r 300 -png doc.pdf page", { check: true, timeoutMs: 300_000 });  await sbx.exec('for f in page-*.png; do tesseract "$f" "${f%.png}"; done', {    timeoutMs: 900_000,  });  const parts = (await sbx.files.glob("page-*.txt")).sort();  const pages = await Promise.all(parts.map((p) => sbx.files.readText(`/workspace/${p}`)));  text = pages.join("\n\f");}console.log(text.slice(0, 500));
Pythonfrom withruntime import Runtimeruntime = Runtime()with runtime.sandboxes.create(    image="pdf",    network={"internet": False},    timeout_seconds=900,    on_lease_end="stop",) as sbx:    with open("invoice.pdf", "rb") as source:        sbx.files.write("/workspace/doc.pdf", source.read())    sbx.exec(["pdftotext", "doc.pdf", "doc.txt"], check=True, timeout_ms=120_000)    text = sbx.files.read_text("/workspace/doc.txt")    if len(text.strip()) < 50:        # A scan: pages to PNG at 300 dpi, then OCR each one.        sbx.exec("pdftoppm -r 300 -png doc.pdf page", check=True, timeout_ms=300_000)        sbx.exec('for f in page-*.png; do tesseract "$f" "${f%.png}"; done', timeout_ms=900_000)        parts = sorted(sbx.files.glob("page-*.txt"))        text = "\n\f".join(sbx.files.read_text(f"/workspace/{p}") for p in parts)    print(text[:500])

The 50-character threshold is a starting point; tune it on your own documents. tesseract imagename outputbase writes outputbase.txt in English by default (Tesseract's documentation, read 25 September 2026). Other languages need their data packages in the image.

Pages for a vision model

Some documents are better read by a model that sees the page: forms, tables with merged cells, handwriting. The same pdftoppm step produces one PNG per page; read them back with files.read and send them to your model. Only the images leave the sandbox, and only because your code chose to fetch them.

Let an agent write the parser

For a stack of similar documents, such as a supplier's invoices, a model can write a small pypdf or regex script that pulls the fields out. That script is untrusted code. Run it in the same sandbox, with the internet already off, and check its JSON against your schema outside the sandbox before you store anything. The loop is the one in AI data pipelines: generate, run with a time limit, validate, retry with the error.

What PDF work needs from its sandbox

Need How Runtime covers it
Parsing files from strangers A microVM with its own kernel; the host enforces network and resources
No exfiltration of the contents Internet off at create; root in the guest cannot turn it back on
OCR's CPU bursts Shared CPU bursts up to the sandbox's vCPUs; you pay for what is measured
Big scans Uploads in 1 MiB pieces checked by SHA-256; set diskMiB for room
A page that makes OCR hang timeoutMs per command, 60 seconds by default and up to 24 hours
Documents already in a bucket Mount S3, R2 or Google Cloud Storage read-only as a directory
Many files at once 100 sandboxes at once on a paid account to start

What it costs

Take 2,000 PDFs a month, a mix of digital files and scans. Each gets a 2 vCPU, 4 GiB sandbox for 40 seconds, and extraction plus OCR uses 30 CPU-seconds:

TextCPU:    2,000 × 30 s / 3,600 × $0.025             = $0.42Memory: 2,000 × 40 s / 3,600 × 4 GiB × $0.0075    = $0.67Total:                                              $1.08

The CPU line is measured use at $0.025 per vCPU-hour and the memory line is $0.0075 per GiB-hour while the sandbox runs (pricing). Digital PDFs that need no OCR finish in a few seconds and cost less. Tesseract is free software, so there is no per-page OCR fee on top. A new account's 50 trial hours cover the first experiments, with no card.

Sources

Related: document conversion, data analysis agent, run untrusted LLM code safely, Python in a sandbox.

Facts on this page were checked on 25 September 2026.