---
title: "Database — Transactions"
aliases: [payload-transactions, payload-db-transactions]
tags: [payloadcms, database, transactions, postgres, mongodb, sqlite]
sources: [raw/database__transactions.md]
created: 2026-05-15
updated: 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.

```ts
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 |

```ts
// 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:

```ts
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()` → returns `transactionID`
- `payload.db.commitTransaction(id)` → finalizes changes
- `payload.db.rollbackTransaction(id)` → discards changes

## Disabling Transactions

**Adapter-level (all operations):**
```ts
// In your DB adapter config
postgresAdapter({ transactionOptions: false })
```

**Per-call (single operation):**
```ts
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 `req` in 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` / `rollbackTransaction` for standalone scripts
- `disableTransaction: true` skips transaction on a single Local API call

## Related

- [[wiki/payloadcms/database-overview|Database — Overview]]
- [[wiki/payloadcms/database-postgres|Database — Postgres]]
- [[wiki/payloadcms/database-mongodb|Database — MongoDB]]
- [[wiki/payloadcms/database-sqlite|Database — SQLite]]
- [[wiki/payloadcms/database-migrations|Database Migrations]]
- [[wiki/payloadcms/local-api|Local API]]

---

*Source: raw/database__transactions.md — https://payloadcms.com/docs/database/transactions*
