Skip to content

Batch and transactions

Batch and transaction helpers accept DbQuery objects. A statement can come from the Table API or raw SQL.

Use database.batch(...) when you want to send several statements together:

Terminal window
await database.batch([
database.table("posts").insert({
id,
title,
body,
authorId,
}),
database.sql(
"insert into audit_logs (id, action, targetId) values (?, ?, ?)",
[auditId, "post.created", id],
),
])

On one-shard databases, batch execution maps naturally to the single D1 target.

Use database.transaction(...) for an atomic logical write:

Terminal window
await database.transaction((tx) => [
tx.table("posts").update({ title, body }).where({ id }),
tx.sql(
"insert into audit_logs (id, action, targetId) values (?, ?, ?)",
[auditId, "post.updated", id],
),
])

The callback receives a transaction context with tx.table(...), tx.sql(...), and tx.statement(...).

For sharded databases, pass a shard hint when the statements belong to one logical partition:

Terminal window
await database.transaction((tx) => [
tx.table("posts").update({ title }).where({ id }),
tx.table("post_events").insert({
id: eventId,
postId: id,
authorId,
type: "title.changed",
}),
], {
shard: {
table: "posts",
key: authorId,
},
})

The table must be listed in db({ sharding }), and key should be the value from that table’s configured key column.

batchPerShard(...) and transactionPerShard(...) run grouped statements with non-atomic batch metadata. They are useful for operations that can be split by shard:

Terminal window
await database.batchPerShard([
database.sql("delete from expired_sessions where expiresAt < ?", [now]),
])

Use transaction(...) for user-facing writes that need atomic behavior.

A batch requires at least one statement. Build the statement list first when a route has optional work:

Terminal window
const statements = []
if (titleChanged) {
statements.push(database.table("posts").update({ title }).where({ id }))
}
if (statements.length > 0) {
await database.batch(statements)
}