Prisma to Drizzle ORM — Bundle Size, Edge Compatibility, and Raw SQL Type Safety Differences with a Production Migration Strategy
I hit a wall the moment I tried to deploy to Cloudflare Workers. The existing PostgreSQL drivers didn't work due to the lack of Node.js net/tls module support, and that's when I realized edge connections required Prisma Accelerate. That's what led me to look into Drizzle, and over the course of late 2025 through 2026 I migrated a few projects myself. The extreme difference in bundle size, native edge runtime support, and immediate type reflection without a code generation step — these three were the core reasons for switching, but there were also quite a few things to watch out for once I actually did it.
This is aimed at Node.js/TypeScript backend and full-stack developers who are already using Prisma or considering it.
Which Choice for Which Situation
Before diving in, let me lay out the decision criteria first. If this single diagram gives you a clear direction, use the rest of the article as supporting material for that decision.
Core Concepts
Design Philosophy: "If You Know SQL, You Know Drizzle"
Prisma and Drizzle start from different premises.
Prisma defines schemas using its own DSL called .prisma and generates client code with prisma generate. The big advantage is that team members unfamiliar with databases can become productive quickly, and in practice this abstraction works well for many teams.
Drizzle went the opposite direction. Its motto is "If you know SQL, you know Drizzle ORM". You define schemas directly in TypeScript files with no code generation step. Add a column and the type is reflected immediately on the next keystroke.
// Drizzle schema definition — a pure TypeScript file
import { pgTable, serial, text, timestamp, decimal } from 'drizzle-orm/pg-core'
export const orders = pgTable('orders', {
id: serial('id').primaryKey(),
userId: text('user_id').notNull(),
amount: decimal('amount', { precision: 10, scale: 2 }).notNull(),
createdAt: timestamp('created_at').defaultNow(),
})// Prisma schema definition — a dedicated DSL file
model Order {
id Int @id @default(autoincrement())
userId String
amount Decimal
createdAt DateTime @default(now())
}Both approaches provide type safety, but the timing and mechanism differ.
Bundle Size: What the Numbers Say
| ORM | Size | Notes |
|---|---|---|
| Drizzle ORM | ~7.4 KB (gzip) | 0 external dependencies |
| Prisma v7 | ~1.6 MB (installed package size) | Introduces WASM query compiler |
| Prisma v6 and below | ~14 MB (installed package size) | Rust binary engine |
Dividing 7.4 KB by 1.6 MB gives roughly a 216× difference. Note that the two figures use different measurement baselines (gzip vs. installed size), so treat the multiplier as a reference point. The gap remains perceptibly large even when compared on an equal basis.
The release of Prisma v7 in November 2025, which replaced the Rust binary with WASM and dramatically reduced its size, is an impressive improvement. Even so, the gap with Drizzle remains.
Why does this actually matter? Cold starts for serverless functions. On Vercel Edge Functions, Cloudflare Workers, and AWS Lambda@Edge, bundle size directly affects function initialization time. Drizzle's 7.4 KB makes achieving sub-500ms cold starts far more attainable.
Edge Runtime Compatibility: This Was the Key Difference
Cloudflare Workers does not support the Node.js net/tls module APIs. Workers has supported TCP itself via the connect() API since 2023, but because the net/tls modules that existing PostgreSQL drivers depend on don't work, you can't connect to a DB through the standard approach. Prior to Prisma v7, resolving this required Prisma Accelerate, a paid proxy service.
Drizzle was designed from the start with this constraint in mind. HTTP-based driver adapters solve this problem at the root.
// Cloudflare Workers + Neon Postgres
import { drizzle } from 'drizzle-orm/neon-http'
import { neon } from '@neondatabase/serverless'
// HTTP-based connection — no Node.js net/tls modules needed
const sql = neon(env.DATABASE_URL)
const db = drizzle(sql)// Cloudflare D1 (Workers built-in SQLite)
import { drizzle } from 'drizzle-orm/d1'
const db = drizzle(env.DB)Native edge support was added in Prisma v7 as well, but Drizzle remains simpler in terms of configuration complexity.
Raw SQL Type Safety: Two Approaches
You might think "what's the point of type safety if you're using raw SQL anyway," but the two ORMs take quite different approaches.
Drizzle's sql<T> template literal
// Manually specify the return type via generics
const result = await db.execute(
sql<{ count: number; revenue: string }>`
SELECT COUNT(*) as count, SUM(amount) as revenue
FROM ${orders}
WHERE created_at > ${startDate}
`
)
// result is typed as { count: number; revenue: string }[]
// Mixing the query builder with raw SQL is also possible
const complexQuery = await db
.select({
id: users.id,
postCount: sql<number>`COUNT(${posts.id})`,
})
.from(users)
.leftJoin(posts, eq(posts.userId, users.id))
.groupBy(users.id)The advantage is that types are reflected immediately without a generation step; the downside is that T must be specified manually by the developer. When the schema changes, it won't be caught automatically at compile time.
Prisma TypedSQL
-- prisma/sql/getOrderStats.sql
SELECT COUNT(*) as count, SUM(amount) as revenue
FROM "Order"
WHERE created_at > $1// Called with auto-generated types after prisma generate
import { getOrderStats } from '@prisma/client/sql'
const result = await prisma.$queryRawTyped(getOrderStats(startDate))
// Fully automatic type inference — no manual specification neededPrisma TypedSQL fully auto-generates input and output types from .sql files. The trade-off is more robust type safety, but types remain stale until prisma generate is run.
Practical Application
Edge Runtime Deployment Scenario
The most common question I get is "how do you connect to a DB in Cloudflare Workers?"
// Hono + Drizzle + Neon (Cloudflare Workers)
import { Hono } from 'hono'
import { drizzle } from 'drizzle-orm/neon-http'
import { neon } from '@neondatabase/serverless'
import { orders } from './schema'
import { eq, desc } from 'drizzle-orm'
type Bindings = {
DATABASE_URL: string
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/orders/:userId', async (c) => {
const sql = neon(c.env.DATABASE_URL)
const db = drizzle(sql)
const userOrders = await db
.select()
.from(orders)
.where(eq(orders.userId, c.req.param('userId')))
.orderBy(desc(orders.createdAt))
.limit(20)
return c.json(userOrders)
})
export default appThis simplicity is exactly why the Hono official docs feature Drizzle examples so prominently.
Relational Queries — Prisma vs Drizzle Comparison
This is the part that trips people up most when moving from Prisma to Drizzle. The key point is that the returned data structure is different. A 1:1 code replacement can lead directly to runtime errors.
// Prisma — fetch relations with include, returns nested objects
const postsWithAuthor = await prisma.post.findMany({
include: {
author: { select: { id: true, name: true } }
},
where: { published: true }
})
// Return structure: { id, title, author: { id, name } }
// Drizzle select join — returns flat objects (different structure!)
const postsWithAuthor = await db
.select({
id: posts.id,
title: posts.title,
authorId: users.id,
authorName: users.name,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
// Return structure: { id, title, authorId, authorName } — no nesting
// Use the relational query API to get the same nested structure as Prisma include
const postsWithAuthor = await db.query.posts.findMany({
where: eq(posts.published, true),
with: {
author: { columns: { id: true, name: true } }
}
})
// Return structure: { id, title, author: { id, name } }To preserve the same result structure as Prisma's include, you need to use with in the relational query API, not the select join approach. If your project has many complex nested relation queries, check this part first.
Production Migration Strategy
The parallel execution strategy is effective for migrating a live service without downtime.
Start by importing the current DB structure as a Drizzle schema.
# Run after configuring drizzle.config.ts
npx drizzle-kit introspectAlways manually review the generated schema file. NOT NULL columns can be incorrectly generated as nullable. In particular, it's recommended to directly cross-check the generated schema against the original DB schema and column constraints.
Here's the code for the parallel execution phase.
import { PrismaClient } from '@prisma/client'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { eq, desc } from 'drizzle-orm'
import * as schema from './drizzle/schema'
const prisma = new PrismaClient()
const queryClient = postgres(process.env.DATABASE_URL!)
const db = drizzle(queryClient, { schema })
// Write new queries with Drizzle
export async function getRecentOrders(userId: string) {
return db
.select()
.from(schema.orders)
.where(eq(schema.orders.userId, userId))
.orderBy(desc(schema.orders.createdAt))
}
// Leave existing Prisma queries untouched
export async function getUserProfile(id: string) {
return prisma.user.findUnique({ where: { id } })
}I personally migrated a small-to-medium SaaS with 20 models and 100 query call sites using this approach. Working alone, it was completable in 2–4 days without downtime; splitting it across a team, 1–2 weeks.
Common Pitfalls — Better to Know in Advance
1. Table name casing issue
// Prisma default table names are PascalCase
// When migrating to Drizzle, use the exact same name
export const users = pgTable('User', { // not 'user'
id: text('id').primaryKey(),
name: text('name').notNull(),
})2. Reimplementing CUID defaults
import { createId } from '@paralleldrive/cuid2'
export const posts = pgTable('Post', {
// Replacement for Prisma's @default(cuid())
id: text('id').primaryKey().$defaultFn(() => createId()),
title: text('title').notNull(),
})3. Automatic timestamp updates
Drizzle lets you configure automatic updates at the column level with the .$onUpdate() hook.
export const posts = pgTable('Post', {
id: text('id').primaryKey(),
title: text('title').notNull(),
updatedAt: timestamp('updated_at')
.defaultNow()
.$onUpdate(() => new Date()),
})
// Columns without $onUpdate must be specified explicitly on every UPDATE
await db
.update(posts)
.set({ title: newTitle, updatedAt: new Date() })
.where(eq(posts.id, postId))4. Decimal type handling
Drizzle returns Decimal columns as string to preserve decimal precision. Prisma returns them as a Prisma.Decimal object (based on decimal.js). Both approaches can lose precision when using the + operator or parseFloat.
import Decimal from 'decimal.js'
const order = await db.select().from(orders).where(eq(orders.id, 1))
// Convert string → Decimal object before arithmetic (preserves precision)
const amount = new Decimal(order[0].amount)
const withTax = amount.mul(1.1).toFixed(2)
// parseFloat is fine for display purposes, but not suitable for financial calculationsPros and Cons
Comparison Table
| Criterion | Drizzle ORM | Prisma v7 |
|---|---|---|
| Bundle size | ~7.4 KB (gzip) | ~1.6 MB (installed package size) |
| External dependencies | 0 | Yes |
| Edge runtime | Native support | Supported from v7 (complex setup) |
| Code generation step | None | prisma generate required |
| Schema definition | TypeScript file | .prisma DSL |
| Raw SQL types | Manual sql<T> specification |
Fully automatic TypedSQL |
| Relational query API | join / with-based |
include / select intuitive |
| Auto timestamp updates | .$onUpdate() hook |
@updatedAt automatic |
| Decimal return type | string |
Prisma.Decimal object |
| Ecosystem maturity | Growing fast | Mature, rich documentation |
| Cold starts | Advantageous | Relatively disadvantaged |
Common Mistakes in Practice
| Mistake | Symptom | Fix |
|---|---|---|
Using introspect output without review |
NOT NULL columns incorrectly generated as nullable | Manually review generated schema file |
| Table name casing mismatch | Table not found error at runtime | Check Prisma default PascalCase and match exactly |
| Direct arithmetic on Decimal | Precision loss | Use decimal.js object for calculations |
Missing updatedAt |
Update timestamp not refreshed | Set .$onUpdate() or specify explicitly on every UPDATE |
| Bulk migration without parallel execution | Downtime or rollback required | Gradual migration via parallel execution |
Closing Thoughts
To summarize the core differences between the two ORMs:
Drizzle pursues the experience of SQL developers writing SQL in TypeScript. If you need a small bundle, edge-ready out of the box, and immediate types without code generation, Drizzle is the right fit.
Prisma improves team productivity through database abstraction. If you need fully automatic types via TypedSQL, an intuitive relational query API, and a rich ecosystem, Prisma v7 remains an excellent choice.
As of 2025–2026, Drizzle is rapidly closing the gap with Prisma in weekly npm downloads, and Drizzle examples are featured prominently in edge-first ecosystems like Hono and Astro DB. That doesn't mean Prisma has become legacy — it's more that a clear divide has emerged: Prisma for large enterprise monoliths, Drizzle for serverless and edge projects.
If you're considering a migration now, here's the order I'd recommend:
3 Steps to Get Started
- Snapshot your existing DB with
npx drizzle-kit introspect— establish a migration baseline. Manually review the generated schema's nullable status against your original DB. - Run Prisma and Drizzle in parallel — coexist both clients on separate connections, and gradually switch over by writing new queries in Drizzle.
- If edge deployment is the goal, start with the driver adapter — connect the right driver for your environment (Neon HTTP or Cloudflare D1) first, then measure bundle size and cold start directly in your actual deployment pipeline.
References
- Drizzle ORM Official Docs - Overview
- Drizzle ORM - Magic sql`` operator
- Drizzle ORM - Vercel Edge Functions Tutorial
- Prisma Official Docs - Prisma vs Drizzle Comparison
- Prisma Official - Introducing TypedSQL
- Prisma Official - Edge Deployment Overview
- Bytebase - Drizzle ORM vs Prisma: Which TypeScript ORM Should You Choose in 2026
- Encore - Drizzle vs Prisma in 2026
- Better Stack - Drizzle vs Prisma: A Guide to Choosing a TypeScript ORM
- MakerKit - Drizzle vs Prisma ORM in 2026: A Practical Comparison
- toolchew - How to Migrate from Prisma to Drizzle ORM: A Step-by-Step Guide
- ormlab - Migrating from Prisma to Drizzle Without Headaches
- DEV Community - I Switched from Prisma to Drizzle on a Live SaaS
- PkgPulse - State of Node.js ORMs 2026
- GitHub - drizzle-team/drizzle-orm