Benchmark · rendered from 2025.11.1.json
292,826 conformant PDF/A-3b invoices per hour
On Linode Dedicated 8 GB (4 dedicated vCPU), eu-central (Frankfurt). Median of 5 timed runs over 10,000 distinct invoices; range 289,269–295,411. Licensed mode (no watermark), veraPDF-3b PASS.
Every figure on this page is interpolated at build time from the machine-readable result artifact — no number is hand-typed. If a value here disagrees with the linked JSON, the artifact wins.
The honest numbers
Throughput, read three ways.
| Aggregate (4 workers) | 292,826 | docs/hour |
| Per core, sustained at full width | 73,206 | docs/hour/core |
| Single worker, uncontended | 120,045 | docs/hour |
Latency a distinct quantity from throughput
| p50 | p95 | p99 |
|---|---|---|
| 36 ms | 121 ms | 180 ms |
Per-document render time. The p99 tail is dominated by the largest invoices — a big-document number, not the typical one. Dividing throughput by latency is meaningless: concurrency is exactly the gap between them.
Scaling — the number, read honestly
Four cores do not do four cores' work. Here is exactly why.
| Workers | 1 | 2 | 4 | 8 |
|---|---|---|---|---|
| Speedup vs. one | 1.00× | 1.79× | 2.44× | 2.30× |
A full 1 / 2 / 4 / 8-worker sweep on the reference box — one physical host, so the headline, the per-core figure, and this curve all share a die. Throughput peaks at 4 workers (the box's 4 vCPUs); the 8-worker run oversubscribes the cores and regresses — more threads than cores is contention, not speedup.
Four cores buy ~2.4×, not 4× — a serial fraction of ~21% at full utilization, and past the core count more workers only contend (8 workers trail 4). That residual isn't a mystery we hand-wave: it is not our factory lock (removing it left throughput unchanged) and not GC (ZGC nil, ParallelGC ~10% within noise) — it sits inside the Saxon/FOP render internals, a named, bounded post-launch target.
The deployment conclusion: scale out with replicas, not up with cores. Several 2–4 vCPU replicas behind a load balancer beat one fat many-core node — which is how stateless containers get deployed anyway. Size against the per-core number as measured on your own hardware — it moved 33% between two boxes of the same plan, so ours is not a constant we can hand you; it is what holds as you add replicas of your instance.
This section shows the page updates against evidence — including evidence against ourselves. That is the point of publishing a benchmark from an artifact.
The corpus
Distinct invoices, not one fixture 10,000 times.
| Dimension | Distribution |
|---|---|
| Lines | 70% small (1–5) · 25% medium (10–40) · 5% large (100–150) |
| Tax scenario | 60% standard @19% · 20% reduced @7% · 15% reverse-charge · 5% zero-rated |
| Text | diacritics, guillemets, µ/Ω/×, Cyrillic — the real glyph path |
10,000 distinct documents from published seed
42. Same seed → byte-identical corpus → identical
hash. The mix is conservative against us: an all-minimal corpus would render
faster and inflate the number.
sha256 88e6c2d1b39fca2452d0ba99a25c4e62da31abcef2256bc1345a30f1a5991b7e
Reproduce it yourself
A seed, an image digest, and about €1 of cloud time.
| Image | ghcr.io/faktorei/render:2025.11.1 |
| Digest | sha256:f8025e739a4678ef39f2c19008a045eb7a7ba3c1ff15b96a86c5fc7935276529 |
| Box | Linode Dedicated 8 GB (4 dedicated vCPU), eu-central (Frankfurt) |
# regenerate the exact corpus, then run the harness
python3 tools/gen_benchmark_corpus.py --count 10000 --seed 42 --out ./bench-corpus
python3 tools/benchmark.py \
--corpus ./bench-corpus \
--engine ./bench_container.sh \
--workers 4 \
--image "ghcr.io/faktorei/render:2025.11.1" \
--image-digest "sha256:f8025e739a4678ef39f2c19008a045eb7a7ba3c1ff15b96a86c5fc7935276529" \
--box "Linode Dedicated 8 GB (4 dedicated vCPU), eu-central (Frankfurt)" \
--out result.json The full methodology (warm-up, median-of-N, per-core pass, veraPDF-on-output) is in the repo's docs/benchmark.md.
Hardware variance — read the absolute number honestly
The plan name is not the silicon.
Two instances of the same "Dedicated 4 vCPU" plan, measured days apart, differed by +51% aggregate and +33% single-core (the two rows in the History table below). The render path between them was byte-identical — the delta is the physical die. Cloud dedicated-vCPU plans span CPU generations, and the host you land on dominates the absolute number more than anything in our software.
So the invariants of this benchmark are the things you can hold constant and verify: the methodology, the corpus sha256, the image digest, and the veraPDF result. The absolute throughput is hardware-bound. Size your deployment against per-core measurements on your own hardware — the evaluation container and the open harness let you produce them in an evening, on the exact silicon you will run in production.
Conformance
Throughput of broken PDFs is worthless.
veraPDF profile 3b was run over the benchmark's own output: 200 / 200 sampled documents passed, 0 failed — PASS. The claim is conformant PDF/A-3b, so the run proves its own output conforms.
History — the page's own audit trail
Every published run.
| Release | Median docs/hour | Box | Image |
|---|---|---|---|
| 2025.11.1latest | 292,826 | Linode Dedicated 8 GB (4 dedicated vCPU), eu-central (Frankfurt) | ghcr.io/faktorei/render:2025.11.1 |
| 2025.11.0 | 194,347 | Linode Dedicated 8 GB (4 dedicated vCPU), eu-central (Frankfurt) | ghcr.io/jdmac26/kontor-render:2025.11.0 |
The 2025.11.0 row has an identical render path
to 2025.11.1 — same code, a different physical host on the same
"Dedicated 4 vCPU" plan. The ~51% aggregate / ~33% single-core gap is
hardware, not software (see Hardware
variance below) — which is exactly why every row carries a box descriptor.
Its image path (ghcr.io/jdmac26/kontor-render) predates the
faktorei rename and is kept as provenance: what was measured, not what is current.