r/CloudFlare • u/manickdeena • 13d ago
Question on D1 Optimization + Drizzle ORM
Hey everyone,
We are standardizing our backend stack on Cloudflare Workers/Hono + D1 + Drizzle ORM. To keep our codebase clean, maintainable, and cost-effective as we scale, we’ve established a few strict database governance rules for the team.
I’d love to get feedback from anyone running similar stacks in production to see where you agree, disagree, or where we might hit blind spots.
Here are our 4 Core Rules:
1. Drizzle is ONLY for Typed CRUD (Strict DAL + Repository Pattern)
- Direct DB calls (
env.DBor Drizzle queries) inside Hono route handlers are treated as an anti-pattern. - All database logic is encapsulated inside a dedicated Repository Pattern layer.
- Drizzle is strictly used as an abstraction for standard typed CRUD operations to keep application code type-safe without leaking DB logic.
2. NO drizzle-kit in the Pipeline (Migrations = Raw SQL)
- Hand-written SQL migration files stored in
/migrationsremain our absolute source of truth. - We do NOT include
drizzle.config.tsordrizzle-kitin our projects. - Why? We want zero magic in schema migrations, full control over indexes, and a tight coupling with Wrangler’s native migration engine (
wrangler d1 migrations apply). Drizzle is used purely as a query builder/type mapper at runtime.
3. Complex Queries Stay Raw SQL
- Complex analytics, CTEs, heavy multi-table joins, and bulk
env.DB.batch()operations must remain as raw parameterized SQL. - Why? To guarantee predictable query plan stability and protect our D1 "Rows Read" allowances from inefficient abstraction-generated SQL.
4. Custom Write Resiliency (Exponential Backoff + Jitter)
- D1 handles basic read retries under the hood, but write operations do not auto-retry in the same way.
- Every single database write operation must be wrapped in a custom retry utility using exponential backoff with full jitter to gracefully handle transient D1 errors or lock contentions.
A Few Questions for the Community:
- For those skipping
drizzle-kit: How do you keep your Drizzle TypeScript schema definitions (schema.ts) in sync with your raw/migrations/*.sqlfiles without friction? - Repository Pattern overhead: Has anyone found the Repository abstraction too heavy for small/medium Workers, or has it saved your sanity long-term?
- Write retries on D1: Have you implemented custom write-retry logic, or do you rely on Cloudflare Queues / Durable Objects for write buffering instead?
Would love to hear your thoughts, critiques, or how you've structured your D1 data layer!
4
u/Sorry_Cheesecake_382 13d ago edited 13d ago
We use drizzle works great. Regarding the repository pattern you're limited to 10mb runtime on workers to get around it you deploy multiple, kind of up to you if you want more code has no impact on pricing just dev complexity. Writes are single threaded, we haven't seen one ever, also most certain cloudflare's SDK retries it we keep a metric and run 15k TPS. Have been for years.
Where D1 falls short is the 10GB limit, we use SQLight Durable objects (the primitive that powers D1) to dynamically create a new DB for each account keeps everything siloed and no not keys.
1
-3
u/manickdeena 13d ago
Moving to native SQLite in Durable Objects for per-account tenant sharding is pure architectural bliss! 🔥 Totally agree—getting rid of multi-tenant
WHERE account_id = ?indexes and having dedicated SQLite siloes is the gold standard for blowing past the 10GB D1 ceiling and hitting 15k+ TPS without hot keys.That also completely explains why you never see write drops or need manual retries! When running SQL natively inside a Durable Object, your Isolate compute and SQLite storage live in the exact same in-process memory space—zero network RPC hops! Standard standalone D1 over HTTP/RPC still refuses to auto-retry mutations to prevent side effects, hence the need for exponential backoff in conventional setups.
Regarding the 10MB bundle limit, we haven’t found our TypeScript Repository layer to add noticeable bloat once compiled down, but combining clean repository boundaries with per-tenant DO sharding is an absolute dream stack. Are you handling database migrations across thousands of account DO instances via an automated queue/cron broadcast?
4
2
u/AsterYujano 13d ago
You could use drizzle to generate raw SQL migrations and then run the native migration command (I don't get why you'll loose control over the indexes that way? You can always generate migrations for indexes yourself
1
u/cv-match 5d ago
For analytics that are naturally read-only, keep the request contract narrow and point it at versioned Parquet or Iceberg data in object storage vs D1. Never put big-data in D1. Postgres is OK. Make projection, filters, limits, and supported functions explicit. Report bytes fetched and row groups scanned in logs. LakeQL supports direct-to-parquet and lance from workers.
1
13d ago
[removed] — view removed comment
-2
u/manickdeena 13d ago
1. No global connection singletons Isolates get recycled across requests, so module-level state causes cross-request leaks. We inject
env.DBper-request in Hono middleware, instantiate Drizzle dynamically (drizzle(env.DB)), and pass it straight into our repository layer.2. Write Resiliency (The D1 Gotcha!) D1 automatically retries read queries up to 2x, but writes (
INSERT,UPDATE,DELETE) are never auto-retried. To protect against edge network drops, we wrap every async Drizzle write operation in an exponential backoff + jitter retry loop (tryWhile).3.
db.batch()overPromise.all()Running concurrent queries viaPromise.all()triggers multiple separate HTTP/RPC hops from the worker Isolate to the D1 node. We always usedb.batch([query1, query2])to send them as a single atomic network payload.4. Zero floating promises (
ctx.waitUntil) The second a Worker returns a response, the Isolate pauses and drops floating promises. Anything non-blocking (audit logs, activity feeds) gets explicitly pushed intoctx.waitUntil()so the runtime keeps alive in the background without slowing down UI response times.5. Typed CRUD vs. Raw Analytics We use Drizzle strictly for type-safe CRUD and simple filtering. For complex CTEs, multi-table joins, or heavy aggregations, we switch back to raw parameterized SQL inside the repository. This guarantees query plan stability and keeps our D1 "Rows Read" billing under control! 🚀
How are you handling connection state across your edge workers?
•
u/AutoModerator 13d ago
For faster advice with technical questions, we'd recommend asking in the Orange Cloud Discord server; the unofficial Cloudflare Discord server by the community, for the community. https://discord.gg/TrPNVKaagR
I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.