faktorei

Benchmark · rendered from 2025.11.5.json

167,636 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 164,738–172,490. 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)167,636docs/hour
Per core, sustained at full width41,909docs/hour/core
Single worker, uncontended77,568docs/hour

Latency a distinct quantity from throughput

p50p95p99
61 ms 215 ms 349 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.70×2.16×1.93×

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.2×, not 4× — a serial fraction of ~28% 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 benchmark 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.

This is the throughput corpus, and it is deliberately narrow: one jurisdiction, one currency, sized to stress the render path rather than the spec surface. It is not the conformance corpus. Coverage of other currencies and national profiles — JPY with no minor unit, AUD under PINT A-NZ, GST rather than VAT — lives in the fixture corpus, which is what the Schematron and pixel gates run on every commit. Throughput and coverage are measured by different corpora on purpose.

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.5
Digestsha256:815b89a9c8647bee81c84a6e12cf7218635016990fdb9c1a15885b43a7036f14
BoxLinode Dedicated 8 GB (4 dedicated vCPU), eu-central (Frankfurt)

Image note: measured on 2025.11.5, the current render path. Throughput is 43% BELOW the 2025.11.1 row (167,636 vs 292,826) despite the same Linode plan, the same region and a byte-identical corpus. That gap is the HOST, not the software: an A/B of the 2025.11.1, .3 and .5 images on one identical machine put all three within ~3% of each other (licensed 3,000-doc runs at workers=4, and unlicensed 480-doc runs to reach 2025.11.1, which predates the ci-2026-2 trust anchor and cannot be run licensed). So the 2025.11.2 render-path change costs nothing measurable. This is the third instance of 'Dedicated 4 vCPU' we have measured and the third distinct number (194,347 / 292,826 / 167,636) -- the plan name does not pin the silicon, which is exactly why the methodology, corpus hash and digest are the invariants rather than the absolute figure. Published without a worker sweep, so the site's scaling curve stops at the core count.

# 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.5" \ --image-digest "sha256:815b89a9c8647bee81c84a6e12cf7218635016990fdb9c1a15885b43a7036f14" \ --box "Linode Dedicated 8 GB (4 dedicated vCPU), eu-central (Frankfurt)" \ --out result.json

Reproducing the published run needs a license. It renders 60,500 documents — 10,000 × 5 timed passes, plus 500 warm-up and a 10,000-document single-worker pass — against an evaluation allowance of 500/day, so evaluation mode cannot complete it, and the harness marks any non-licensed result do-not-publish because the watermark changes what is being measured. Want to verify it? Ask for a time-boxed license and we will issue one.

What you can do unlicensed is the thing this page argues actually matters: the harness takes --runs and --warmup, so a small corpus fits inside a single evaluation day and gives you a per-core figure on your own hardware. The methodology, the corpus sha256 and the image digest are the parts we ask you to hold us to — those are checkable without us.

The full methodology (warm-up, median-of-N, per-core pass, veraPDF-on-output) is docs/benchmark.md, alongside every published result artifact.

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.5latest 167,636 Linode Dedicated 8 GB (4 dedicated vCPU), eu-central (Frankfurt) ghcr.io/faktorei/render:2025.11.5
2025.11.1 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.