d1 insert loop dies around 1000 rows with too many API requests by single worker invocation Limits & Errors
CSV import Worker on the paid plan. Files are anywhere from 200 to 3,000 rows. The loop is the obvious thing:
for (const r of rows) {
await env.DB.prepare("INSERT INTO items (sku, name, price) VALUES (?, ?, ?)")
.bind(r.sku, r.name, r.price).run()
}
Small files import fine. Anything over about a thousand rows and I get Error 1015: Too many API requests by single worker invocation partway through, with some rows already committed. Is there a per-request query cap I have missed somewhere?
@deploy_friday_ok · 2d ago · 3 replies
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():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.
Reply
Report
@ridgeline_rowan · 3d ago
Chunks of 100 took a 2,400 row import from timing out to 1.9 seconds. I had read "subrequest" as meaning outbound fetch only and never connected it to database calls.
Reply
Report
@orm_tamsin · 4d ago
If the column list is fixed you can go further and build one statement with 100 value tuples — one subrequest and one parse instead of 100 prepared bindings. Mind the SQLite bound parameter ceiling of 999 while you do it: with three columns that is 333 rows a statement, with six it is 166.
Reply
Report