On April 30, the Drizzle team tagged v1.0.0-rc.1 - the first release candidate after a year of betas. A second RC followed on May 5. The headline number from their own benchmark is a 25-30% latency reduction and a jump from 14,500 to 15,300 requests per second. The headline claim is bolder: with one config flag, Drizzle is now "as fast as using raw driver and mapping results by hand."
That claim is the whole story of this release. For most TypeScript ORMs, the cost of being an ORM lives in a single hot loop - the code that turns the driver's raw rows into typed objects. Drizzle just compiled that loop away.
The ORM tax has always been row mapping
Every ORM is doing essentially the same thing on the read path. The driver hands you back an array of arrays, or a row buffer, or - in some Postgres drivers - text representations that still need parsing. Then your ORM walks each row, walks each column, looks up the schema, applies the right transform (timestamp needs parsing, jsonb needs JSON.parse, bytea needs decoding), and builds an object.
In a typical web request, this happens once per row, for every row in the result. With Prisma's old query engine, the work happened in Rust and then crossed back over a serialization boundary. With Drizzle, until now, it happened in a generic TypeScript loop that branched on column metadata at every step. Generic loops are kind to write and unfriendly to V8's optimizer. The branches do not inline. The shapes are polymorphic. The result is the kind of overhead that benchmarks expose and production teams quietly absorb.
rc.1's answer is to stop running a generic loop and start generating a specific one.
JIT mappers, opt-in
The new API is a single flag:
const db = drizzle({ ..., jit: true });
const query = db.select().from(users).prepare();
// drizzle generates a jit mapper which will then be reused
// across every invocation
server.get("users", async (c) => {
const rows = await query.execute(); // as fast as the raw driver
return c.json(rows);
});The trick is the .prepare() call. When you prepare a query, Drizzle now knows the exact shape of the result - which columns, in which order, with which types - and it emits a row mapper specialized to that shape. No branches. No metadata lookups. No polymorphic call sites. Just straight-line code that pulls the fields it needs from the driver's row buffer and assembles the typed object.
The mapper is cached on the prepared statement, so the JIT cost is paid exactly once. Every subsequent query.execute() reuses it. The pattern is borrowed from how high-performance database libraries in C and Rust have always worked - prepare once, hot-loop forever - but it has been awkward to express in idiomatic TypeScript ORMs. Drizzle's bet is that .prepare() was always the right interface, and most users just were not using it.
It is opt-in for a reason. JIT generation costs a small amount of memory and a single up-front pass. For an app with thousands of distinct queries that each run rarely, that math may not pay off. For an app with a few dozen queries that each run thousands of times per second - which is most web apps - it absolutely does.
Underneath: a codec system
The JIT mapper is the user-visible piece. The change underneath is larger. Drizzle rebuilt the Postgres internals around what the team calls codecs - per-type readers and writers that normalize the differences between drivers.
This matters because Postgres in TypeScript is not one driver. It is node-postgres, postgres.js, @neondatabase/serverless, Bun's built-in bun:sql, AWS Data API, and Cloudflare's Hyperdrive. Each one returns timestamp differently. Each one disagrees about jsonb. The neon-http driver had a bytea corruption bug. Bun's driver double-stringified JSON in some paths.
The codec layer is where those differences get reconciled before the data ever reaches your code. The release notes call out "fixed bun-sql/postgres timestamp timezone truncation, json[b] data double stringification" and "fixed neon-http bytea data corruption" as direct consequences. Three multi-month-old GitHub issues - #3018, #5090, #5287 - all close out as fixed by the same architectural change.
This is the unglamorous half of a 1.0. You do not pick an ORM because the codec table is well-organized. You stay on one because three years in, the edge cases still resolve.
The casing API got rewritten
rc.1 also introduces a breaking change to the casing API, and the team's own framing of it is worth reading directly:
In rc.1 we've finally reworked our legacy casing API of const db = drizzle({..., casing: "camel" }) which turned out to not be a proper solution, it required duplication in drizzle-orm instantiation and drizzle-kit config and introduced and set of endless bugs all around query builder chain.The old approach set casing once on the db instance, then required you to repeat it in your drizzle.config.ts so the CLI agreed. The two could drift. Worse, casing leaked through joins and subqueries in ways the query builder could not always track.
The new API moves casing onto the schema declaration itself:
import * as d from "drizzle-orm/pg-core";
const users = d.snakeCase.table("users", {
id: d.serial().primaryKey(),
email: d.text().unique(),
fullName: d.text(), // -> full_name
createdAt: d.timestamp().defaultNow(), // -> created_at
});
// schemas inherit casing for every table inside them
const schema2 = d.snakeCase.schema("schema2");
const ordersInSchema2 = schema2.table("orders", { /* ... */ });It is more verbose. It is also less wrong. Casing is now a property of the table definition, which is the thing that maps to columns - exactly where it should live. The query builder does not have to thread an option through every chain. Drizzle Kit reads the same source of truth as the runtime.
Four open issues - #5112, #5282, #4181, #4209 - all reference the same casing inconsistencies and all get closed as fixed by this change. Pre-1.0 is the only time an ORM gets to clean these up without breaking everyone's apps. They used the window.
Effect v4 lands as a first-class integration
The release ships native support for Effect v4, through a new drizzle-orm/effect-postgres module:
import { PgClient } from "@effect/sql-pg";
import * as PgDrizzle from "drizzle-orm/effect-postgres";
import * as Effect from "effect/Effect";
import { relations } from "../relations";
const PgClientLive = PgClient.layer({
url: process.env.PG_CONNECTION_STRING!,
});
const DB = PgDrizzle.make({ relations }).pipe(
Effect.provide(PgDrizzle.DefaultServices),
);
const program = Effect.gen(function* () {
const db = yield* DB;
const users = yield* db.select().from(usersTable);
}).pipe(Effect.provide(PgClientLive));
await Effect.runPromise(program);This is more interesting than it looks. Effect's pitch has always been that promise-based TypeScript silently swallows the things that matter - errors, retries, resource lifetimes, observability - and that a properly-typed effect system gets them back. Pairing it with an ORM was awkward when the ORM was promise-shaped. Drizzle's Effect integration treats the query builder as a service that yields effects, which means transactions, retries, and error channels compose with the rest of an Effect-based app. For teams already on Effect, it removes the last awkward boundary.
What is still beta
The release notes include a Drizzle for LLM agents (preview) line and not much else. There is a public llms-full.txt of the docs already, which is the most basic shape that takes. The real version - a schema-aware tool surface an agent can use to introspect, query, and migrate safely - is not in this RC. It is the kind of feature that wants to ship in 2.0, not be retrofit into 1.0 in a hurry.
Also still missing from the v1 line: MSSQL and CockroachDB support exists since beta.2 but without RQBv2. Relational Queries V2, the bigger relations API, is shipping per-dialect and is not uniform yet. The v1 roadmap still calls v1 "98%" - the remaining 2% is where most of the migration friction will live.
One quieter note worth flagging: beta.20, on March 27, fixed an SQL injection in sql.identifier() and sql.as() - CWE-89, found by external researchers. If you are running anything between beta.19 and beta.20, that is the upgrade to do first, RC or not.
Why this is the moment to look again
Drizzle has spent most of its public life as "the lightweight Prisma alternative." That framing was always thin - Drizzle's actual pitch is that you write SQL-shaped TypeScript, not that you avoid a Rust binary. But the performance gap stayed real, because Prisma's query engine could do tricks the JavaScript side could not. Then Prisma rewrote its query engine in TypeScript through 2025, ate a chunk of that lead, and the comparison stopped being about runtimes.
rc.1 is Drizzle's response. The bet is that if both ORMs are now pure TypeScript, the one that compiles its hot path wins. The jit: true flag is small. The architectural choice underneath it - per-query specialization with prepared statements as the seam - is the kind of decision that compounds across a 1.x line.
If you have a Drizzle app in production: pin the version, run the casing migration on a branch, flip jit: true on your hottest endpoint and measure. If you are choosing an ORM right now, this is the first release where it is honest to say the ORM tax is gone. The remaining question is whether the casing rework lands cleanly enough that 1.0 ships without an rc.3.
