3.8 KiB
3.8 KiB
| title | aliases | tags | sources | created | updated | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Database — Transactions |
|
|
|
2026-05-15 | 2026-05-15 |
Overview
Payload wraps all data-changing operations in database transactions by default (where supported). If any error is thrown during a request, all DB changes in that request are rolled back atomically.
- PostgreSQL / SQLite (with config): full ACID transactions
- MongoDB: requires a replicaset connection (standalone instance → no transactions)
- SQLite: transactions disabled by default — pass
transactionOptions: {}to enable
How It Works
Each request starts a transaction and stores its ID at req.transactionID. Payload hooks and Local API calls that receive the same req automatically join the transaction.
const afterChange: CollectionAfterChangeHook = async ({ req }) => {
// Joins the parent transaction — rolled back if the request fails
await req.payload.create({
req, // ← key: pass req to opt in
collection: 'my-slug',
data: { some: 'data' },
})
}
Async Hooks — Gotcha
If you fire-and-forget (no await), do not pass req:
| Pattern | Behavior |
|---|---|
await call({ req }) |
Safe — joined to transaction, rolled back on error |
call({ req }) (no await) |
Dangerous — failure returns misleading OK, data not committed |
call({}) (no req) |
Safe — runs independently, not rolled back with parent |
// WRONG — unawaited + same req = misleading success response
const dangerouslyIgnoreAsync = req.payload.create({ req, collection: 'x', data: {} })
// CORRECT — unawaited + no req = independent, isolated
const safelyIgnoredAsync = req.payload.create({ collection: 'x', data: {} })
Direct Transaction Control
For custom scripts and endpoints outside normal request flow:
const transactionID = await payload.db.beginTransaction()
try {
await payload.update({
collection: 'posts',
data: { some: 'data' },
where: { slug: { equals: 'my-slug' } },
req: { transactionID }, // ← pass as minimal req object
})
await payload.db.commitTransaction(transactionID)
} catch (error) {
await payload.db.rollbackTransaction(transactionID)
}
API:
payload.db.beginTransaction()→ returnstransactionIDpayload.db.commitTransaction(id)→ finalizes changespayload.db.rollbackTransaction(id)→ discards changes
Disabling Transactions
Adapter-level (all operations):
// In your DB adapter config
postgresAdapter({ transactionOptions: false })
Per-call (single operation):
await payload.update({
collection: 'posts',
data: { some: 'data' },
where: { slug: { equals: 'my-slug' } },
disableTransaction: true,
})
Key Takeaways
- Payload transactions are on by default for all mutations — no opt-in needed for standard hooks
- Always pass
reqin hooks to join the parent transaction; omit it for fire-and-forget calls - MongoDB needs a replicaset — standalone dev MongoDB has no transaction support
- SQLite transactions are opt-in:
transactionOptions: {} - Use
beginTransaction/commitTransaction/rollbackTransactionfor standalone scripts disableTransaction: trueskips transaction on a single Local API call
Related
- wiki/payloadcms/database-overview
- wiki/payloadcms/database-postgres
- wiki/payloadcms/database-mongodb
- wiki/payloadcms/database-sqlite
- wiki/payloadcms/database-migrations
- wiki/payloadcms/local-api
Source: raw/database__transactions.md — https://payloadcms.com/docs/database/transactions