Context
The question that separates Drizzle from Kysely comes before any benchmark: do you want an ORM, or just a typed way to write SQL? Drizzle is a lightweight ORM. You declare the schema in TypeScript (pgTable, mysqlTable, etc.), and from it you get type inference, a query builder with relations, and drizzle-kit to generate and apply migrations. Kysely is not an ORM: it's a typed query builder that mirrors SQL almost 1:1. There's no schema declared as the source of truth inside the library; you supply a Database interface with the table types (by hand or generated by kysely-codegen from the database) and write selectFrom(...).select(...).where(...) the way you'd write the SQL itself.
That difference decides who owns the schema. In Drizzle, TypeScript is the source of truth: drizzle-kit diffs the schema and emits the SQL for migrations. In Kysely, the database is the source of truth: kysely-codegen introspects the tables and generates the types, and you write migrations by hand with a minimal migrator the library ships, with no generator that diffs the schema. Both are pure TypeScript with no native binary, so both run well on edge and serverless (this is where the comparison with a Rust-engine ORM stops applying). What's left as a tangible difference is package cost (about 8.4 kB gzip for Drizzle against 38.7 kB for Kysely) and reach: Drizzle pulls more downloads (11.4M/wk against 8.8M/wk) and carries drizzle-kit and Drizzle Studio under one umbrella, while Kysely stays strictly focused on building queries.
When to choose Drizzle
Drizzle makes sense when you want the guarantees of an ORM without giving up queries that read like SQL: a schema declared in one place, types inferred from it, migrations versioned by drizzle-kit, and relation helpers when the model has recurring joins.
import { pgTable, serial, text, integer } from "drizzle-orm/pg-core";
import { eq } from "drizzle-orm";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
name: text("name").notNull(),
teamId: integer("team_id").notNull(),
});
const rows = await db.select().from(users).where(eq(users.teamId, 1));
The users above is both the schema and the origin of the types: db.select() already knows id is number and name is string, with no manual codegen step. When the schema changes, drizzle-kit generates the matching migration, so the change history lives alongside the code. Add the relations API for recurring joins, Drizzle Studio for inspecting data, and a runtime that is just TypeScript (no binary), and you have a complete data layer that still runs on edge with no native-engine cold start. The cost is accepting an ORM layer: its own schema-definition conventions and a query builder that, SQL-like as it is, remains an abstraction on top of literal SQL.
When to choose Kysely
Kysely makes sense when you don't want an ORM, you want SQL. It gives full type-safety over queries you write yourself, in the form closest to SQL, without hiding joins, subqueries, or aggregate functions behind an object API.
import { Kysely } from "kysely";
interface Database {
users: { id: number; name: string; team_id: number };
}
const db = new Kysely<Database>({ dialect });
const rows = await db
.selectFrom("users")
.select(["id", "name"])
.where("team_id", "=", 1)
.execute();
The Database interface is the contract: you write it by hand or let kysely-codegen generate it from the existing database. From it, each method mirrors a SQL clause, and inference follows along: the select(["id", "name"]) above narrows the type of rows to exactly those two columns, with no any and without pulling fields you didn't ask for. There are no magic relations and no intermediate engine; what you write is what runs against the database. The trade-off is that Kysely doesn't do an ORM's job: you write migrations by hand with the library's minimal migrator (there's no schema diff like drizzle-kit's), keeping the Database interface in sync with the database is your responsibility (or your codegen's, in the pipeline), and the package is larger, about 38.7 kB gzip against Drizzle's 8.4 kB.
Verdict
The axis is simple: Drizzle is an ORM, Kysely is not. If you want a data layer that owns the schema, generates migrations from it (drizzle-kit), and offers relations, Drizzle delivers all of that in a runtime that is still just TypeScript, which here weighs in its favor, including on the smaller size (8.4 kB against 38.7 kB). If you want to write SQL with full type-safety and nothing else, with no ORM abstraction between your code and the database, Kysely is the more honest tool: every query is SQL you recognize, with the safety net of types.
Because both are pure TypeScript, the runtime tiebreaker that usually decides one ORM against another doesn't show up here: the question goes back to how much abstraction over SQL you accept and who owns the schema. A team that already has a database and thinks in SQL tends to do well with Kysely plus kysely-codegen, leaving the database as the source of truth. A team starting from scratch that wants schema, migrations, and relations coming from one place finds a more complete layer in Drizzle. Swapping one for the other in a project that already works rarely pays off: the choice is one of data-access style, not correctness, and rewriting queries that already pass their tests just to change syntax is effort with no clear return.