Date: June 15, 2026
Machine: WSL2, Intel i7-13700HX, 24 logical CPUs, 7.6 GiB RAM
Toolchain: Go 1.26.4, Python 3.13.10, Node.js v22.20.0
Branch: feat/performance-improvements
Multi-run validation (June 15, 2026): key harnesses rerun 10× (Zerodha) or multiple times (k6) after the initial full-suite pass.
| Category | Winner | Throughput (validated) | Notes |
|---|---|---|---|
| Zerodha weighted (80/15/5, 5000 iter) | GoPDFLib | 2,646 avg / 2,898 peak ops/sec | 10-run mean; ECDSA P-256 signing |
| Zerodha retail (48 workers) | GoPDFSuit | 1,978 avg / 2,234 peak ops/sec | 10-run mean; see §2b |
| Data table (2000 rows, 5000 iter) | GoPDFLib | 189 ops/sec | PDF/A-4 compliant |
| HTTP weighted workload (48 VUs × 35s) | gopdfsuit | 910 avg / 973 peak req/sec | 2-run validation; see §3d |
| vs Gotenberg (same k6 harness) | gopdfsuit | 89× faster | 910 vs 10 req/sec |
| Pure Python data table | Typst | 9.4 ops/sec | Bundled typst 0.11.0 |
| Python bindings (Zerodha weighted) | pypdfsuit | 223 ops/sec | CGO + multiprocessing |
Dataset: 2,000 user records (ID, Name, Email, Role, Description).
Command: bash sampledata/benchmarks/run_all_benchmarks.sh
| Runtime | Workload | Workers | Iterations | Min (ms) | Avg (ms) | P95 (ms) | Max (ms) | Throughput |
|---|---|---|---|---|---|---|---|---|
| GoPDFLib | Data table | 48 | 5000 | 95.42 | 251.82 | 401.88 | 1021.48 | 180.00 |
| GoPDFLib | Zerodha retail | 48 | 48 | 10.85 | 19.00 | 24.31 | 25.56 | 1,782.05 |
| GoPDFSuit | Zerodha retail | 48 | 48 | 10.33 | 20.55 | 28.36 | 29.33 | 1,586.72 |
| pypdfsuit | Zerodha retail | 10 | 10 | 44.48 | 45.92 | 49.55 | 49.55 | 112.60 |
| FPDF2 | Data table | 10 | 10 | 4686.97 | 4887.06 | 5077.91 | 5077.91 | 1.96 |
| jsPDF | Data table | 10 | 10 | 1896.27 | 2034.31 | 2235.80 | 2235.80 | 4.16 |
| PDFKit | Data table | 10 | 10 | 1270.23 | 1334.29 | 1403.72 | 1403.72 | 5.04 |
| pdf-lib | Data table | 10 | 10 | 1726.79 | 1820.55 | 1933.00 | 1933.00 | 4.73 |
| Typst | Data table | 10 | 10 | 898.41 | 991.37 | - | 1052.62 | 9.42 |
Typst note:
typst/bench.shfailed because the bundled binary is typst 0.11.0, which does not support--pdf-standard a-2b. Results above are from a manual rerun without the PDF/A flag. See Known Issues.
Node.js note: jsPDF, PDFKit, and pdf-lib failed on the first
run_all_benchmarks.shpass due to missingnpm install. They were rerun successfully after installing dependencies.
- GoPDFLib - 180 ops/sec (92× faster than FPDF2)
- Typst - 9.4 ops/sec
- PDFKit - 5.0 ops/sec
- pdf-lib - 4.7 ops/sec
- jsPDF - 4.2 ops/sec
- FPDF2 - 2.0 ops/sec
- GoPDFSuit - 1,978 ops/sec (10-run mean)
- GoPDFLib - 1,782 ops/sec
- pypdfsuit - 113 ops/sec (10-worker harness; CGO + Python overhead)
Workload: 80% Retail / 15% Active / 5% HFT, 5000 iterations, 48 workers, ECDSA P-256 signing.
| Run | Throughput (ops/sec) | Avg Latency (ms) |
|---|---|---|
| 1 | 2,452.01 | 19.07 |
| 2 | 2,377.29 | 19.68 |
| 3 | 2,581.94 | 17.74 |
| 4 | 2,456.85 | 19.00 |
| 5 | 2,804.22 | 16.67 |
| 6 | 2,860.05 | 16.28 |
| 7 | 2,619.42 | 17.51 |
| 8 | 2,846.40 | 16.39 |
| 9 | 2,898.07 | 15.89 |
| 10 | 2,561.59 | 18.34 |
| Mean | 2,645.78 | 17.66 |
| Median | 2,600.68 | 17.63 |
| σ | 192.09 | 1.33 |
| Peak | 2,898.07 | - |
The initial single-run value of 1,321 ops/sec (during the full benchmark suite) was a cold-start outlier. After dedicated 10-run validation, sustained throughput is ~2,500–2,900 ops/sec.
Workload: single retail contract note, 48 iterations, 48 workers.
| Run | Throughput (ops/sec) | Avg Latency (ms) |
|---|---|---|
| 1 | 2,234.24 | 15.20 |
| 2 | 2,180.95 | 16.42 |
| 3 | 2,176.16 | 16.01 |
| 4 | 2,018.49 | 17.21 |
| 5 | 1,980.93 | 19.08 |
| 6 | 1,863.94 | 19.45 |
| 7 | 1,842.00 | 20.08 |
| 8 | 1,831.87 | 19.07 |
| 9 | 1,829.56 | 17.76 |
| 10 | 1,817.01 | 18.23 |
| Mean | 1,977.51 | 17.85 |
| Median | 1,922.43 | 18.00 |
| σ | 166.19 | 1.62 |
| Peak | 2,234.24 | - |
| Runtime | Command | Throughput | Avg Latency | Min | Max |
|---|---|---|---|---|---|
| pypdfsuit | make bench-pypdfsuit-zerodha |
223.09 ops/sec | 188.75 ms | 2.07 ms | 3,094 ms |
pypdfsuit vs GoPDFLib (10-run mean): 223 ÷ 2,646 = 8.4% of native Go throughput on the weighted harness.
| Target | Throughput | Key metric |
|---|---|---|
bench-gopdflib-data |
189.43 ops/sec | 5000 iter, 48 workers, data table |
bench-gopdfsuit-zerodha |
1,978 avg / 2,234 peak ops/sec | 10-run validation; 48 iter retail |
bench-gopdflib-zerodha |
2,646 avg / 2,898 peak ops/sec | 10-run validation; 5000 iter weighted |
bench-pypdfsuit-zerodha |
223.09 ops/sec | 5000 iter weighted mix (single run) |
make bench-handler-all - Gin handler, financial_report.json:
| Benchmark | ns/op | MB/s | B/op | allocs/op | ~ops/sec |
|---|---|---|---|---|---|
FinancialReport (serial) |
321,648 | 226.82 | 333,392 | 289 | ~3,109 |
FinancialReport_Parallel |
59,994 | - | 339,895 | 289 | ~16,669 |
make bench-pdf-micro - internal/pdf, Rows2000 (10-run average):
| Benchmark | ns/op | MB/s | B/op | allocs/op | ~ops/sec |
|---|---|---|---|---|---|
GenerateTemplatePDF/Rows2000 |
~15.3M | ~108 | ~5.8M | ~59,900 | ~65 |
GenerateTemplatePDF_WrapEnabled/Rows2000 |
~30.8M | ~57 | ~6.0M | ~61,300 | ~32 |
GoPdfSuit |
~15.4M | - | ~5.8M | ~59,900 | ~65 |
benchtime=5s, 40 workers, Go 1.26.4:
| Workload | GoPDFKit (pdf/s) | GoPDFLib (pdf/s) | gopdflib lead |
|---|---|---|---|
text_short |
114,825 | 206,298 | +80% |
text_240_lines |
14,077 | 23,741 | +69% |
table_180_rows |
11,307 | 28,870 | +155% |
table_900_rows |
2,460 | 7,621 | +210% |
invoice_40_rows |
39,240 | 105,514 | +169% |
png_table_180_rows |
6,949 | 32,077 | +362% |
png_rows_60 |
4,792 | 42,548 | +788% |
Winner: gopdflib on all 7 workloads.
k6 throughput varies with run order and system load. Dedicated make bench-k6 runs (2-run validation) averaged 910 req/s (peak 973 req/s). Runs executed back-to-back during the full multi-library suite measured ~747–769 req/s.
| Run | Timestamp | http_reqs/s | http med | http p99 | pdf_gen med | pdf_gen p99 | Retail avg | HFT avg |
|---|---|---|---|---|---|---|---|---|
| 1 | 2026-06-15 01:14 IST | 972.74 | 13.57 ms | 276.11 ms | 15 ms | 444 ms | 17.3 ms | 375.6 ms |
| 2 | 2026-06-15 01:15 IST | 846.37 | 15.38 ms | 337.18 ms | 17 ms | 527 ms | 19.4 ms | 440.9 ms |
| Mean | - | 909.56 | 14.48 ms | 306.65 ms | 16 ms | 485.5 ms | 18.4 ms | 408.3 ms |
| Run | http_reqs/s | http med | http p99 |
|---|---|---|---|
| Suite run 1 | 758.93 | 17.07 ms | 380.08 ms |
| Suite run 2 | 747.10 | 17.78 ms | 385.97 ms |
| Suite run 3 | 767.65 | 17.07 ms | 380.08 ms |
| Mean | 757.89 | 17.31 ms | 382.04 ms |
| Target | http_reqs/s | http med | http p99 |
|---|---|---|---|
make bench-k6 (gopdfsuit, 2-run mean) |
909.56 | 14.48 ms | 306.65 ms |
make bench-k6 (gopdfsuit, peak) |
972.74 | 13.57 ms | 276.11 ms |
make bench-gotenberg (single run) |
10.17 | 4.37 s | 8.50 s |
gopdfsuit vs Gotenberg (2-run mean): 909.56 ÷ 10.17 = 89.4× higher HTTP throughput.
Peak run: 972.74 ÷ 10.17 = 95.6×.
k6 latency breakdown (peak run - 972.74 req/s):
| Segment | avg | p99 |
|---|---|---|
| Retail | 17.3 ms | 75 ms |
| Active | 23.8 ms | 92.7 ms |
| HFT | 375.6 ms | 624.8 ms |
| Stack | Harness | Workload | Throughput | Unit |
|---|---|---|---|---|
| GoPDFLib | bench-gopdflib-zerodha (10-run mean) |
Zerodha weighted | 2,646 | PDF/s |
| GoPDFLib | bench-gopdflib-zerodha (10-run peak) |
Zerodha weighted | 2,898 | PDF/s |
| GoPDFSuit | bench-gopdfsuit-zerodha (10-run peak) |
Zerodha retail | 2,234 | PDF/s |
| GoPDFSuit | bench-gopdfsuit-zerodha (10-run mean) |
Zerodha retail | 1,978 | PDF/s |
| GoPDFLib | run_all single |
Zerodha retail | 1,782 | PDF/s |
| gopdfsuit | bench-k6 (2-run mean) |
HTTP weighted | 910 | req/s |
| gopdfsuit | bench-k6 (peak) |
HTTP weighted | 973 | req/s |
| gopdfsuit | bench-k6 (suite runs mean) |
HTTP weighted | 758 | req/s |
| GoPDFLib | bench-gopdflib-data |
Data table | 189 | PDF/s |
| pypdfsuit | bench-pypdfsuit-zerodha |
Zerodha weighted | 223 | PDF/s |
| pypdfsuit | run_all single |
Zerodha retail | 113 | PDF/s |
| Typst | data table | Data table | 9.4 | PDF/s |
| PDFKit | data table | Data table | 5.0 | PDF/s |
| pdf-lib | data table | Data table | 4.7 | PDF/s |
| jsPDF | data table | Data table | 4.2 | PDF/s |
| Gotenberg | bench-gotenberg |
HTML Chromium | 10.2 | req/s |
| FPDF2 | data table | Data table | 2.0 | PDF/s |
-
GoPDFLib dominates in-process workloads. On the Zerodha weighted gold standard (10-run validated) it averages 2,646 ops/sec (peak 2,898) - roughly 12× faster than pypdfsuit on the same harness and 1,300× faster than FPDF2 on the data-table task.
-
pypdfsuit is viable for Python production use. At 223 ops/sec on the full weighted Zerodha mix (signed PDF/A contract notes including 2.4 MB HFT documents), it far exceeds pure-Python libraries (FPDF2 at 2 ops/sec) while retaining the full gopdfsuit feature set.
-
HTTP API is production-grade.
make bench-k6sustains 910 req/sec (2-run mean) with a peak of 973 req/sec, 0% errors, ~14 ms median HTTP latency, and ~307 ms p99 - aligned with historical ~825 req/s averages. Runs executed during the full benchmark suite averaged ~758 req/s. -
Gotenberg is not competitive for this workload. Chromium-based HTML rendering caps at ~10 req/sec vs gopdfsuit's ~910 req/sec - an 89× gap on identical k6 scenarios (peak: 96×).
-
Single-shot benchmarks can underreport. The first GoPDFLib weighted run during the full suite measured only 1,321 ops/sec; dedicated 10-run validation showed the true steady-state band is 2,400–2,900 ops/sec. The same effect applies to k6 when run back-to-back with other harnesses. Prefer dedicated multi-run validation for headline numbers.
-
gopdflib beats GoPDFKit on every micro-workload in the apples-to-apples compare harness, with leads ranging from +69% (text) to +788% (PNG rows).
-
Typst is the fastest non-Go data-table option at 9.4 ops/sec, but the bundled binary (0.11.0) lacks PDF/A support flags used by
bench.sh.
| Target | Reason |
|---|---|
bench-gopdflib-zerodha-x2/x5/x10 |
Statistical multi-run variants; single run captured |
bench-pypdfsuit-zerodha-x2 |
Same |
bench-gopdflib-data-pprof |
pprof capture variant (longer) |
bench-pdf-macro |
Extended synthetic table sizes |
bench-pdf-typst |
Requires compare build tag |
bench-gopdfkit-html |
Opt-in HTML subset, needs Chrome |
bench-k6-light |
Reduced k6 + pprof: 24 VU × 15s, lower RAM (WSL / shared machine) |
bench-k6-retail/1k/1500/smoke/spike/soak |
k6 scenario variants |
bench-suite / bench-suite-full |
Meta-targets composing above |
| Issue | Impact | Workaround |
|---|---|---|
typst/bench.sh uses --pdf-standard a-2b |
Fails on bundled typst 0.11.0 | Compile without flag; upgrade binary to 0.12+ |
run_all_benchmarks.sh needs npm install first |
jsPDF/PDFKit/pdf-lib fail silently | Run npm install in sampledata/benchmarks/ |
System typst in PATH may shadow bundled binary |
Wrong version picked | Use explicit path to typst-x86_64-unknown-linux-musl/typst |
| k6 run after full benchmark suite | Throughput drops ~20–30% (758 vs 973 req/s) | Run make bench-k6 as a dedicated harness for headline HTTP numbers |
| k6 server dies mid-run (~70%) on WSL | OOM / resource contention when parallel benchmarks run | Run make bench-k6-light in isolation; stop run_all_benchmarks and other heavy jobs first |
# Setup
make bench-setup
cd sampledata/benchmarks && npm install
cd bindings/python && ./build.sh && pip install -e .
# Multi-library suite
bash sampledata/benchmarks/run_all_benchmarks.sh
# Makefile targets
make bench-gopdflib-zerodha
make bench-pypdfsuit-zerodha
make bench-gopdfsuit-zerodha
make bench-gopdflib-data
make bench-handler-all
make bench-pdf-micro
make bench-gopdfkit-compare
make bench-k6
make bench-k6-light # 24 VU × 15s - use on WSL or when full run OOMs
make bench-gotenberg
# Multi-run validation
for i in $(seq 1 10); do make bench-gopdflib-zerodha; done
for i in $(seq 1 10); do make bench-gopdfsuit-zerodha; done
for i in 1 2; do make bench-k6; done