Ask

Ola Bergqvist

@deploy_friday_ok

Twelve years in backend work, currently the entire infrastructure department at a fifteen-person company. I deploy on Fridays because rollbacks should be boring, and I will explain how to make yours boring too.

18 credit Contributor

From answers
0
From questions
18

Joined January 20, 2025 · 0 followers · 0 following

d1 insert loop dies around 1000 rows with too many API requests by single worker invocation

Every D1 query counts as a subrequest, and subrequests are capped per Worker invocation — 1,000 on paid, 50 on free. Your loop spends one per row, so it dies at row 1,000 with beautiful consistency.

Use batch():

const stmt = env.DB.prepare(
  "INSERT INTO items (sku, name, price) VALUES (?, ?, ?)"
)
for (const chunk of chunked(rows, 100)) {
  await env.DB.batch(chunk.map(r => stmt.bind(r.sku, r.name, r.price)))
}

One batch() call is one subrequest regardless of how many statements are in it, and it runs as an implicit transaction. 3,000 rows becomes 30 subrequests instead of 3,000, and it will also be about an order of magnitude faster because you stop paying a round trip per row.

Past roughly 10,000 rows you will run into CPU time instead, and the answer there is a Queue consumer processing a few hundred rows per message.

92 · in/workers-and-d1 ·

how do people actually run d1 migrations in production, is wrangler apply it

Yes, wrangler d1 migrations apply <db> --remote is the real answer, not the toy one. The setup most teams converge on:

  • SQL files in migrations/, either hand-written or produced by drizzle-kit generate
  • --local against your dev database while you work
  • CI runs the --remote apply as a step before wrangler deploy

Two things to internalise coming from Rails.

There is no down migration. Wrangler records applied filenames in a d1_migrations table and only ever moves forward. So you write expand-then-contract: add the nullable column, deploy code that writes both old and new, backfill, and only in a later migration drop the old one. A rollback is a new migration, always. This feels primitive for about a month.

And take an export before anything destructive:

wrangler d1 export app-db --remote --output backup.sql

It is slow and you will be glad exactly once, which is enough.

87 · in/workers-and-d1 ·

r2 multipart upload from the browser fails cors preflight but curl works fine

Alternative that deletes this whole category of problem: upload through a Worker with an R2 binding instead of presigning. No signing, no CORS matrix, no x-amz-* guessing, and authorisation happens in the same place as the rest of your auth.

Costs you Worker CPU and it is not the right answer for every file size, but for the common case it is a smaller system.

36 · in/workers-and-d1 ·

one durable object per room or one DO plus a KV index for 500 live rooms

Think about placement before you have users in two hemispheres. idFromName puts the object near whoever touched it first, so a room created by someone in Sydney and joined by five people in Berlin means every one of those five is paying a Pacific round trip on every edit. If rooms are regional in nature, look at location hints at creation time.

31 · in/workers-and-d1 ·

d1 or neon behind a worker for 30 req/s and about 2 gb of data

The thing I would actually go and look at is your joins. D1 is SQLite, and while EXPLAIN QUERY PLAN exists it tells you much less than a Postgres plan does, so query surprises are harder to diagnose. If you have three-join pages today, you will have five-join pages in a year, and the debugging story matters more than the throughput number.

28 · in/workers-and-d1 ·