Regex log extract
POST /api/logs
Stdlib re — both runtimes can run it. Real CloudWatch log-scrubbing pattern.
| Cold | p50 | p99 | OK | |
|---|---|---|---|---|
| pymode | 43 ms | — | — | — |
| Pyodide (CF) | 1827 ms | — | — | — |
The same Python source on both runtimes — log regex extract, markdown
rendering with markdown_it_py + jinja2, and HTML scraping with
BeautifulSoup4. Three pymode workers (one per workload, each AOT-built)
under *.pymode.teamchong.net, one Pyodide worker handling all three
endpoints at pyodide-bench.chong1222.workers.dev.
Same Python source on both runtimes. p50/p99 come
from N back-to-back requests sent by your browser when this page
loaded (first request discarded as a warmup, so the numbers
reflect steady-state warm behaviour).
Cold is the "Worker Startup Time" Cloudflare
reports immediately after wrangler deploy — it's the
cost of bringing an isolate up before any snapshot system
kicks in. You pay it on the very first request to a fresh deploy,
or the next request after CF evicts an idle isolate (typically a
few minutes of no traffic). You do not pay it on every
request — once an isolate is warm in a PoP, subsequent requests in
that PoP skip the startup entirely. In production both runtimes
also use memory snapshots that reduce this number further than the
deploy-time signal shows.
Auto-running benchmark…
POST /api/logs
Stdlib re — both runtimes can run it. Real CloudWatch log-scrubbing pattern.
| Cold | p50 | p99 | OK | |
|---|---|---|---|---|
| pymode | 43 ms | — | — | — |
| Pyodide (CF) | 1827 ms | — | — | — |
POST /api/markdown
CommonMark renderer + Python template engine. Both runtimes can run the same packages from pip via uv.
| Cold | p50 | p99 | OK | |
|---|---|---|---|---|
| pymode | 61 ms | — | — | — |
| Pyodide (CF) | 1827 ms | — | — | — |
POST /api/scrape
Real-world page → structured title/headings/links via BeautifulSoup4. Both runtimes support bs4 via pip.
| Cold | p50 | p99 | OK | |
|---|---|---|---|---|
| pymode | 55 ms | — | — | — |
| Pyodide (CF) | 1827 ms | — | — | — |
POST /api/kv-fanout
Read 20 keys from CF KV in a loop, aggregate to JSON. Exercises the Python ↔ JS boundary 20× per request — host-import overhead shows up here.
| Cold | p50 | p99 | OK | |
|---|---|---|---|---|
| pymode | 60 ms | — | — | — |
| Pyodide (CF) | 545 ms | — | — | — |
POST /api/numpy
np.array([1..5]).mean() + sum(). Real numpy import + native array ops. pymode loads numpy via the python-numpy.wasm variant (linalg stubbed); Pyodide loads its pre-built numpy wheel.
| Cold | p50 | p99 | OK | |
|---|---|---|---|---|
| pymode | 34 ms | — | — | — |
| Pyodide (CF) | 1559 ms | — | — | — |
Each pymode workload runs on its own deployed worker under
*.pymode.teamchong.net with tailored preimports — that
way the Worker Startup Time CF reports at deploy varies per workload.
The Pyodide comparison runs at pyodide-bench.chong1222.workers.dev.
First request of each measurement is a discarded warmup so the
reported p50/p99 reflect steady-state behaviour. Latencies include
network round-trip from your browser.
Cold is the Worker Startup Time Cloudflare reports right after
wrangler deploy. You pay it on the first request to a fresh deploy,
and on the next request after an idle isolate is evicted from a PoP.
You do not pay it on every request. In production, Cloudflare’s
Python Workers also use memory snapshots, so an actual cold-isolate
spin-up in production is faster than this deploy-time signal —
especially for Pyodide. The pymode number is closer to the real
cold-isolate cost because pymode’s snapshot is baked into the wasm
at deploy via wizer, so every isolate boot reads from the same
pre-initialised state.
p50 / p99 are warm-request latencies measured live by your browser. First request of each measurement is discarded as a warmup. Numbers include network RTT from your machine to the closest Cloudflare PoP.
| pymode | Pyodide on CF | |
|---|---|---|
| Python | Upstream CPython 3.13 from source | Pyodide’s port (currently lagging CPython by ~1 minor version) |
| Deploy flow | pymode deploy — AOT-compiles a wasm tailored to the app’s imports (~10 min) | pywrangler deploy — Pyodide loads packages at deploy time (~seconds) |
| Pure-Python pip packages | Any (uv pip install at deploy) | Any compatible with Pyodide |
| Compiled C extensions | Compile via zig cc to wasm32-wasi (manual) | Curated set Cloudflare ships (numpy, pandas, Pillow, lxml, etc.) |
| Host imports (KV, R2, D1, fetch) | AOT-specialised: hot-path call sites patched into direct JS stubs | JsProxy/PyProxy translation per call |
| DurableObject classes | yes | yes (via workers-py SDK) |
| Per-deploy startup | tens of ms | ~1-2 seconds |
| Vendored deploy artifact | one wasm per workload (~10 MB raw) | ~1.5 MB modules + Pyodide runtime loaded by CF |
git-CPython compiled via
zig cc. If you want 3.13 features today, you get them; Pyodide’s
CPython port lags the upstream release cadence.wasm32-wasi via zig cc. The long tail of
niche scientific libs or internal C modules is where this matters.python-numpy.wasm variant but hasn’t wired numpy into
the AOT deploy path yet, so a non-stdlib numpy app deploys cleanly
on Pyodide and doesn’t on pymode.workers-py; pymode
is a community project against the same platform.This page exists because pymode is a personal experiment and the official Python Workers are the obvious baseline to compare against. The numbers are what they are.