faktorei

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,826docs/hour
Per core, sustained at full width73,206docs/hour/core
Single worker, uncontended120,045docs/hour

Latency a distinct quantity from throughput

p50p95p99
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.

Workers1248
Speedup vs. one1.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.

DimensionDistribution
Lines70% small (1–5) · 25% medium (10–40) · 5% large (100–150)
Tax scenario60% standard @19% · 20% reduced @7% · 15% reverse-charge · 5% zero-rated
Textdiacritics, 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

The generator (Apache-2.0) →

Reproduce it yourself

A seed, an image digest, and about €1 of cloud time.

Imageghcr.io/faktorei/render:2025.11.1
Digestsha256:f8025e739a4678ef39f2c19008a045eb7a7ba3c1ff15b96a86c5fc7935276529
BoxLinode 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.

ReleaseMedian docs/hourBoxImage
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.