最近、こんな記事を見ました。

このブログしかり、Cloudflare で D1 を使って色々アプリケーションを動かしているので、ちゃんと自分の手で確かめてみよう、と思い確認してみました。
結論
- SQLite はトランザクションをサポートしている。
- Cloudflare D1 ではネイティブトランザクションをサポートしていないが、
batch()によって制約はあるもののトランザクションを実現できる。
実際にやってみる
SQLite はトランザクションを正式にサポートしています。
ローカル D1 環境で、実際にトランザクションが機能するかどうかを確認していきます。
wrangler init d1test
category は Hello World Example、type は Worker Only
を選択しました。
"d1_databases": [
{
"binding": "DB",
"database_name": "d1test-db",
}
]
セットアップ直後ではローカルDBは構築されていませんが、以下のようなコマンドを実行することで作成されます。
npx wrangler d1 execute d1test-db --local --command "select * from users"
以下のように sqlite ファイルが作成されていればOKです。
% ls .wrangler/state/v3/d1/miniflare-D1DatabaseObject
e7352547963de7050bd7d94658afc4fe78b61811b7815da12d90be8e863abf4d.sqlite
e7352547963de7050bd7d94658afc4fe78b61811b7815da12d90be8e863abf4d.sqlite-shm
e7352547963de7050bd7d94658afc4fe78b61811b7815da12d90be8e863abf4d.sqlite-wal
SQLite コマンドでデータベースに接続します。
sqlite3 .wrangler/state/v3/d1/miniflare-D1DatabaseObject/e7352547963de7050bd7d94658afc4fe78b61811b7815da12d90be8e863abf4d.sqlite
sqlite> .table
_cf_METADATA users
CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NULL
);
最初に1レコードだけ登録しておきます。
INSERT INTO users(name) VALUES ('Alice');
次に、トランザクションの中で2つ目のレコードを登録し、コミットを待機します。
BEGIN TRANSACTION;
INSERT INTO users(name) VALUES ('Bob');
別のセッションで、users テーブルを確認します。
sqlite> select * from users;
1|Alice
まだ Bob は登録されていません。
COMMIT;
コミットをすると、Bob が現れました。
sqlite> select * from users;
1|Alice
2|Bob
普通にトランザクションが実行できました。実際の D1 環境でやってみましょう。
D1 を作成します。
npx wrangler d1 create d1test-db
"d1_databases": [
{
"binding": "DB",
"database_name": "d1test-db",
"database_id": "31964bda-483d-4b61-9960-021fce69bf54"
},
]
users テーブルを作成します。
npx wrangler d1 execute d1test-db --remote \
--command "CREATE TABLE users (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name TEXT NULL);"
npx wrangler d1 execute d1test-db --remote --command "PRAGMA table_info(users);"
sqlite インターフェースが使用できないので、以下のように無理やりトランザクションを開始してみます。
npx wrangler d1 execute d1test-db --remote --command "BEGIN TRANSACTION;"
以下のようにエラーになりました。Cloudflare D1 では、BEGIN TRANSACTION 句は実行できないようです。
⛅️ wrangler 4.63.0
───────────────────
Resource location: remote
🌀 Executing on remote database d1test-db (31964bda-483d-4b61-9960-021fce69bf54):
🌀 To execute on your local development database, remove the --remote flag from your wrangler command.
✘ [ERROR] A request to the Cloudflare API (/accounts/a87f4a10ba648f14c384e0496f347814/d1/database/31964bda-483d-4b61-9960-021fce69bf54/query) failed.
To execute a transaction, please use the state.storage.transaction() or
state.storage.transactionSync() APIs instead of the SQL BEGIN TRANSACTION or SAVEPOINT statements.
The JavaScript API is safer because it will automatically roll back on exceptions, and because it
interacts correctly with Durable Objects' automatic atomic write coalescing. [code: 7500]
If you think this is a bug, please open an issue at:
https://github.com/cloudflare/workers-sdk/issues/new/choose
🪵 Logs were written to "/Users/a2-ito/Library/Preferences/.wrangler/logs/wrangler-2026-02-07_00-49-11_051.log"
トランザクションが使えないとどういう時に問題が発生するか?
トランザクションが必要となる具体例を挙げてみます。
- ユーザプロファイルの作成
- ユーザが新規登録されると、ユーザテーブルに追加し、各種情報をプロファイルテーブルに追加する
- 残高
- 商品を購入すると、残高が減算される
- 在庫引当
- 在庫を引当て、注文管理テーブルに登録する
このようなケースにおいて、あるテーブルは更新できたがあるテーブルは更新できなかったという状態になってしまうとシステム的に問題になりうるので、そうならないようトランザクションによって原子性を担保します。
在庫引当処理を例として、実際にトランザクション実装を試してみましょう。
今回の例では、在庫テーブル items から在庫引当を行い、在庫引当ができたら注文テーブル reservations に登録します。
トランザクション実装箇所の抜粋です。
await db.transaction(async (tx) => {
const result = await tx
.update(items)
.set({
quantity: sql`${items.quantity} - ${qty}`,
})
.where(sql`${items.id} = ${itemId} AND ${items.quantity} >= ${qty}`);
if (result.meta.changes === 0) {
throw new Error("OUT_OF_STOCK");
}
await tx.insert(reservations).values({
id: reservationId,
itemId,
quantity: qty,
status: "reserved",
});
});
この処理を実行すると、以下のようにエラーが返ってきます。
{"success":false,"message":"Failed query: begin\nparams: "}%
drizzle ORM でトランザクション処理を記述すると SQLite に対してネイティブトランザクションが発行されます。ローカル SQLite で直接 BEGIN TRANSACTION 句を実行すると実行できましたが、SDK を経由して実行するとエラーになりました。
D1 でトランザクションを実装する
Cloudflare D1 では、ネイティブトランザクションをサポートしない代わりに、batch() というオプションが提供されています。

Batched statements are SQL transactions. If a statement in the sequence fails, then an error is returned for that specific statement, and it aborts or rolls back the entire sequence.
こちらの batch() でトランザクションを実装できます。今回試した在庫引当処理を batch() で書き直すと以下のようになります。(トランザクション箇所のみ抜粋)
const results = await db.batch([
db
.update(items)
.set({
quantity: sql`${items.quantity} - ${qty}`,
})
.where(sql`${items.id} = ${itemId} AND ${items.quantity} >= ${qty}`),
db.insert(reservations).values({
id: reservationId,
itemId,
quantity: qty,
status: "reserved",
}),
]);
今回の在庫引当のケースでは、上記の方法でトランザクションを実行することができました。しかし、drizzle ORM によるトランザクションが使えません。また、読んだ値に応じて処理を分岐したりIDを発行したりなど、トランザクションの中でアプリケーションロジックを挟みたいケースには対応できません。
よって、D1 におけるトランザクション実装は、現状大きな制約がある状況といってもよいでしょう。
まとめ
D1 がトランザクションをサポートしない件については、以下のスレッドで議論されています。サポートする予定も今のところなさそうです。
Cloudflare D1 は個人的には使いやすくコストも安い非常によい RDB サービスだと思います。 ただ、SQLite のネイティブトランザクションをサポートしないことで ORM によるトランザクション実装ができず、独自の batch API によってトランザクションを実装するしかありません。
業務アプリケーションを開発するには制約が大きすぎるなと思いますが、個人開発では引き続き愛用させてもらいたいなと思います。