Plate 04
Ajv vs Zod: 3.9M vs 1.1M Validations per Second
ajv 8.20.0 (+ajv-formats 3.0.1) vs zod 4.6.5 on the same nested user shape as zod-vs-valibot. Warm medians (2k warmup + 9x50k): Ajv validate ok 3,933,337 ops/s vs zod.safeParse 1,138,406 (~3.5x). Bad input: Ajv allErrors 4,094,452 vs Zod 435,537 (~9.4x). Setup: Ajv new+addFormats+compile median 6.47 ms vs Zod build 0.17 ms; break-even ~10k validations. Browser gzip: Ajv runtime 41 KB, Ajv standalone 1.8 KB, Zod classic 93 KB, zod/mini 6.8 KB. Fixture /workspace/bench-ajv-zod/.
Aditya Challa6 min read
I validated one nested user object 50,000 times per round on this box: Ajv 8.20.0 median 3.93M ops/s, Zod 4.6.5 safeParse median 1.14M ops/s, about 3.5× in Ajv's favor. The catch is setup: compiling the Ajv schema took a median 6.5 ms against 0.17 ms to build the Zod schema.
Short answer: pick Ajv for long-running Node services that validate JSON Schema at high volume. Stay on Zod for TypeScript-first code, short-lived functions and browser forms. No affiliate links in this post.
What I tested
- Machine: 8 vCPU Intel Xeon / 15 GB RAM shared Linux cloud box, tested 6 Oct 2026 about 00:36–00:38 IST.
- Versions: ajv 8.20.0 with ajv-formats 3.0.1, zod 4.6.5, Node 22.20.0, esbuild for the bundle check.
- Fixture: /workspace/bench-ajv-zod/. The same user shape as my Zod vs Valibot test (uuid, email, int age 0–120, tags array, nested profile, enum role), written once as JSON Schema and once as a Zod object. One good payload and one bad payload (bad email, negative age, invalid role).
- Timing: 2,000 warm calls, then 9 rounds of 50,000 calls each. Medians reported. Setup cost: 30 fresh builds each of
new Ajv()+addFormats+compileand of the Zodz.object. - Findings: Ajv validate ok 3,933,337 ops/s (12.7 ms / 50k); zod.safeParse ok 1,138,406 (43.9 ms); zod.parse ok 1,075,899. Invalid input: Ajv first-error 6,664,006 ops/s, Ajv allErrors 4,094,452, zod.safeParse 435,537. Both rejected the bad payload with 3 errors in all-errors mode.
- One surprise: Ajv's lead grew on bad input. With allErrors on, it was about 9.4× faster than Zod at collecting the same 3 issues.
- Not tested: Ajv JTD mode, async or custom keywords, Zod transforms and codecs, TypeScript compile time, Bun/Deno, edge runtimes, Valibot (covered separately).
How wide was the validation gap?
| Path | Median ops/s | Median ms / 50k |
|---|---|---|
| Ajv validate, good input | 3,933,337 | 12.7 |
| zod.safeParse, good input | 1,138,406 | 43.9 |
| zod.parse, good input | 1,075,899 | 46.5 |
| Ajv validate, bad input (first error) | 6,664,006 | 7.5 |
| Ajv validate, bad input (allErrors) | 4,094,452 | 12.2 |
| zod.safeParse, bad input | 435,537 | 114.8 |
Ajv compiles each schema into a plain JavaScript function, so the hot path is a few type checks and regex tests. Zod walks its schema objects on every call. If you are choosing between Zod and the lighter TypeScript-first option instead, my Zod vs Valibot numbers use the same user shape.
When does Ajv's compile cost eat the win?
Compile time is the part most benchmarks skip. Over 30 fresh builds, Ajv setup had a median of 6.47 ms (range 5.7–11.2 ms) and Zod 0.17 ms. The very first Ajv compile in a cold process took 32.5 ms against 7.4 ms for the first Zod build plus parse.
At my measured speeds, Ajv saves about 0.62 µs per valid object. That pays back the 6.3 ms setup gap after roughly 10,000 validations. A serverless function that checks a few hundred requests per cold start comes out ahead on Zod. A long-running API server passes that point within seconds. Compile once at module load and reuse the validator. Never compile inside a request handler.
Does the bundle size change the call?
| Browser build (esbuild minify, ESM) | Minified | gzip -9 |
|---|---|---|
| Ajv runtime + ajv-formats + compile | 134,306 B | 41,149 B |
| Ajv standalone (precompiled validator only) | 6,628 B | 1,780 B |
| Zod 4 classic, this schema | 453,301 B | 92,748 B |
| zod/mini, this schema | 20,242 B | 6,755 B |
In the browser, Ajv standalone was the smallest by far: 1.8 KB gzip, because the validator is generated at build time. Runtime Ajv also calls new Function, which a strict Content-Security-Policy without unsafe-eval blocks. Standalone code avoids that too. Zod classic shipped 93 KB gzip for this one schema. zod/mini cut that to 6.8 KB. Installed trees were about 1.35 MB for Ajv, ajv-formats and their 4 dependencies against 6.1 MB for Zod. For tracking shipped weight in CI, see size-limit vs bundlesize.
Which one should you pick?
| Situation | My pick |
|---|---|
| Node API validating JSON bodies at volume | Ajv, compiled once at startup |
| You already publish JSON Schema or OpenAPI | Ajv |
| TypeScript-first app wanting inferred types | Zod |
| Short-lived serverless function | Zod (setup is 0.17 ms vs 6.5 ms) |
| Browser form validation | Ajv standalone or zod/mini |
| Strict CSP with no unsafe-eval | Ajv standalone or Zod |
Types are the real trade-off. Zod gives you z.infer for free. With Ajv you either write JSONSchemaType<T> by hand or generate types from the schema. If your validators run in tests, my Jest vs Vitest run covers the runner side. If a swap leaves the old library installed, knip vs depcheck finds it. For linting the schema files themselves, see Biome vs ESLint/Prettier.
The format checks are not identical. ajv-formats and Zod use different email and uuid patterns, so test your own edge cases before switching.
Bottom line: on 50k validations of one user object, Ajv 8.20.0 hit 3.93M ops/s against 1.14M for Zod 4.6.5, but its 6.5 ms compile only pays off after about 10,000 calls. Who should not switch: TypeScript teams whose Zod schemas double as their types and whose validation is nowhere near a hot path.
How this was made: I installed Ajv and Zod on the ShopperCove box, timed warm validation rounds and fresh schema builds, measured esbuild minify+gzip sizes and recorded the JSON. The write-up was drafted with AI help and checked against that output.
Sources
- https://ajv.js.org/
- https://ajv.js.org/standalone.html
- https://www.npmjs.com/package/ajv
- https://github.com/ajv-validator/ajv-formats
- https://zod.dev/
- https://www.npmjs.com/package/zod
Related
- https://www.shoppercove.com/blog/zod-vs-valibot
- https://www.shoppercove.com/blog/size-limit-vs-bundlesize
- https://www.shoppercove.com/blog/jest-vs-vitest
- https://www.shoppercove.com/blog/knip-vs-depcheck
- https://www.shoppercove.com/blog/biome-vs-eslint-prettier
- https://www.shoppercove.com/blog/tsx-vs-ts-node
- https://www.shoppercove.com/blog/swc-vs-esbuild-babel
- https://www.shoppercove.com/blog/msw-vs-nock
Lab evidence
What I found running this
Hands-on on ShopperCove box 6 Oct 2026 ~00:36-00:38 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 22.20.0). ajv 8.20.0 + ajv-formats 3.0.1 vs zod 4.6.5. Fixture /workspace/bench-ajv-zod/: one user schema (uuid, email, int age 0-120, tags[], nested profile, enum role) as JSON Schema and as z.object; good + bad payloads. 2,000 warm calls then 9 rounds x 50,000. Medians: Ajv ok 3,933,337 ops/s (12.7 ms/50k); zod.safeParse ok 1,138,406 (43.9 ms); zod.parse ok 1,075,899; Ajv fail first-error 6,664,006; Ajv fail allErrors 4,094,452; zod.safeParse fail 435,537. Both report 3 errors on bad payload. Setup (30 fresh builds): Ajv 6.474 ms median (5.7-11.2), Zod 0.174 ms; cold first Ajv compile 32.5 ms vs Zod first build+parse 7.4 ms. esbuild minify/gzip9: Ajv runtime+formats+compile 134,306/41,149 B; Ajv standalone 6,628/1,780 B; Zod classic schema 453,301/92,748 B; zod/mini schema 20,242/6,755 B. Disk ~1.35 MB vs ~6.1 MB. Raw: res-ajv-zod.json + res-bundle.json. Not tested: JTD, async/custom keywords, Zod transforms, TS compile time, Bun/Deno, edge. No affiliate.
Related links
Plate 48
Zod vs Valibot: 1.14M vs 0.97M Parses
zod 4.6.5 vs valibot 1.5.0 on one nested user schema (uuid/email/age/tags/profile/enum). Warm medians after 2k warmup + 9×50k: zod.parse 1,144,494 ops/s vs valibot.parse 968,187 (~1.18×). safeParse fail: valibot 697,760 vs zod 415,523 (~1.7×). esbuild minify+gzip9: 452,999/92,636 B vs 85,126/15,422 B. Package trees ~6.1 MB vs ~1.9 MB. Fixture /workspace/bench-zod-valibot/.
5 Oct 2026
Plate 47
bun test vs Vitest: 136ms vs 1.67s on 300 Tests
bun test 1.4.2 vs Vitest 5.0.3 on 300 TypeScript tests in 60 files: warm wall medians 136 ms vs 1,673 ms (~12x); Vitest --no-isolate 765 ms (~5.6x). A 5-file vitest-API check (vi.fn, vi.mock, fake timers, spyOn, it.each, snapshot) passed 7/7 under Bun.
5 Oct 2026
Plate 46
esbuild-register vs jiti: 343ms vs 540ms Cold
esbuild-register 3.6.0 vs jiti 2.7.0 on a 61-file TypeScript graph: cold wall medians 343 ms vs 540 ms (~1.57x); after jiti disk cache warm, 185 ms vs esbuild-register 350 ms. Same printed result 970.
5 Oct 2026