Skip to content

Benchmarks

Run them yourself:

bash
bash scripts/bench.sh

It packs the workspace, installs the tarballs into an empty project alongside drizzle-orm@1.0.0-rc.4, generates from one schema, and compares DRZL's output against drizzle-orm/zod's on the same table.

Not part of CI. A benchmark on a shared runner measures the runner, and a number that moves with the weather gets ignored or, worse, chased.

The table

Eight columns shaped like a real one, four of them carrying a CHECK:

ts
export const users = pgTable('users', {
  id: integer().notNull(),
  email: varchar({ length: 255 }).notNull(),
  name: text().notNull(),
  age: integer().notNull(),
  tier: text().notNull(),
  score: integer().notNull(),
  active: boolean().notNull(),
  createdAt: timestamp({ mode: 'string' }).notNull(),
}, (t) => [
  check('age_c', sql`${t.age} >= 18`),
  check('score_c', sql`${t.score} BETWEEN 0 AND 100`),
  check('tier_c', sql`${t.tier} IN ('free', 'pro', 'team')`),
  check('name_c', sql`length(${t.name}) >= 2`),
]);

Results

One run of the script above, at commit 1c92eaa, on one machine: an Intel Core Ultra 9 285H with 32 GB, Ubuntu 24.04 under WSL2, Node v22.22.0. Every cell comes from that single run, so the rows are consistent with each other rather than assembled from whichever run last touched each one.

DRZLdrizzle-orm
constraints the database enforces, reproduced4/40/4
generated bytes for this table1992not a file
parses/sec, row accepted3,207,8923,793,579
parses/sec, row mistyped157,101163,050
parses/sec, row violates a CHECK155,8913,626,688

Read the ratios and not the absolute numbers, and know how much a ratio is allowed to move before it means anything: three consecutive runs on this machine put the accepted-row gap between 15% and 21% and the mistyped-row gap between 3% and 7%, while the first two rows did not move at all. A cell that shifts by less than that has told you nothing.

Reading them honestly

The last row is not a comparison. drizzle-orm runs at over three million parses per second on a row that violates a CHECK because it accepts the row. It is fast in the way that returning true unconditionally is fast. Throughput is only comparable between two validators that enforce the same thing, and these two do not.

The first row is the whole point. Four constraints the database will refuse; DRZL reproduces all four, drizzle-orm/zod reproduces none. Every one of those is a row that passes validation and then fails at the database, which is the worst place to find out.

The second-to-last row is the one that matters most in practice. An API spends its validation time on requests that fail, and there the two are within 4% on this run and within 7% across the three: the cost of a rejected parse is dominated by building the error, not by the checks.

On the happy path DRZL is 15% slower on this run, and within 21% on any of the three, and that is the real cost of enforcing four extra constraints. It used to be 35%. A numeric CHECK was emitting

ts
z.number().int().gte(-2147483648).lte(2147483647).refine((v) => v >= 18)

a bound that can never fail, plus a closure saying what the bound should have said. It now folds into the range:

ts
z.number().int().gte(18).lte(2147483647)

which is faster, 28% smaller for this table, and produces zod's own error, Too small, expected number to be >=18, with the bound machine-readable on the issue rather than inside a string a generator wrote.

What is not measured here

Correctness against the database, which is checked separately and continuously. verify-packed.sh runs the emitted schemas against three real databases on every commit: Postgres in-process via PGlite, SQLite via node:sqlite, and MySQL as a CI service container.

    1476 probes against a real Postgres (41 columns)
    agree with the database: DRZL 1103, drizzle-orm 1042
    DRZL closer than drizzle-orm on 61, further on 0

    403 rows read back through the driver (41 columns)
    rejected by DRZL: 66, of which drizzle-orm also rejects: 66

    59 CHECK probes against a real Postgres (15 constrained columns)
    rows Postgres rejects and the validator accepts: DRZL 0, drizzle-orm 24

    9 defaulted columns, 9 reproduced by applyDefaults
    32 CHECK probes against a real SQLite (10 constrained columns)
    37 probes against a real MySQL

Both directions are asked, because a column's write type and its read type are not the same type. The first block sends each probe through an INSERT and grades the Insert schema on the answer. The second writes a row, reads it back through the driver, and grades the Select schema on the value a caller actually receives. geometry is written as point(1 2) and read back as [1, 2]; char(4) takes 'ab' and returns 'ab '; boolean takes the string 'yes' and returns true. Grading one schema on the other's question is right only where the two coincide.

The read direction is gated absolutely rather than against drizzle-orm. Elsewhere a validator is allowed to be stricter than a coercing driver, since that is what a validator is for, but that reasoning is about untrusted input: a row read back came out of the database through the very driver the schema describes, so a schema refusing it fails on real rows and no amount of agreement makes it correct.

Two more benchmarks

bench.sh measures one generator on one table of eight columns. Two more scripts answer what a table that size cannot, and neither is in CI either, for the same reason:

bash
bash scripts/bench-validators.sh
bash scripts/bench-scale.sh

bench-validators.sh runs the table above through all four validator generators, each against drizzle-orm's own module for that library, on the same rows for the same number of repetitions. Every figure is a median with the range beside it, because one number implies a precision a handful of runs does not support.

It then prices the thing people choose TypeBox for. A CHECK that SQL counts in characters cannot become a JSON Schema keyword, because every keyword counts UTF-16 units, so it becomes a registered kind. TypeCompiler cannot inline one: it emits kind('DrzlRowCheck', 0, value), a call out of the compiled function and into the registry. The script measures tables that differ in nothing else, so what a kind costs is a subtraction rather than a claim, and it does the same for arktype's narrow, which is where that generator's CHECKs live and which costs proportionally more.

Compilation itself is not where it lands. TypeCompiler.Compile takes the same tens of microseconds whether the schema carries four registered kinds or none; what changes is the speed of the checker it hands back.

bench-scale.sh generates schemas of 25 to 400 tables with scripts/gen-wide-schema.mjs, which is where the fixture is defined: the arguments in the run are the description of it. It then splits one drzl generate into phases, end to end from the shell and again from inside the process, and attributes a real run's CPU by package.

The answer is not per-table work. On a 200-table schema the generators and validation-core together account for under a tenth of the run; the largest single item is jiti transpiling the schema module the first time it sees it. That result is cached in os.tmpdir()/jiti rather than under the project, and the cache is keyed by content, so the cost returns after every edit to the schema and drzl watch pays it on every save.

Need a custom template or integration? DM @omardulaimidev on X.