# Transactions

> Status: Available. Every transaction is strictly serializable, including across shards. Clients must retry conflicts and look up unknown outcomes.

| | |
|---|---|
| **Works** | Strict serializability as the default and only isolation level. Atomic, isolated commits across shards inside one database. Interactive transactions and atomic HTTP batches. |
| **Differs** | Locking is no-wait, so a conflicting transaction fails at once with 40001 and the client retries. |
| **Missing** | Savepoints. Transactions that span separate databases. Server-side automatic retry. |
| **Operational impact** | A commit waits for conditional writes to the bucket. |
| **Evidence** | Fault-injection tests, deterministic simulation and bounded TLA+ models. Not an independent audit. |

## The guarantee

Transactions are **strictly serializable**: the result is the same as running them one at a time, in an order consistent with real time. If transaction A is acknowledged before transaction B starts, B sees A.

This holds for every transaction. There is no weaker default to opt out of, and a stale read is never given silently.

It also holds **across shards**. A transaction that touches several shards of one database commits atomically and in isolation: prepare on each shard, one write-once decision object, then resolve. Index entries are written in the same attempt as the row, so a unique index is never briefly wrong.

The guarantee is per database. There is no transaction across two separate databases.

## What you must handle

Two error codes are part of normal operation. No stock PostgreSQL driver handles either for you.

### 40001: conflict, retry the whole transaction

Locking is **no-wait**. A transaction that meets a conflicting lock is not queued; it fails with `40001`. An ownership change while a transaction is open gives the same code.

Roll back, then run the whole transaction again. Do not retry only the failed statement.

```python
import psycopg

def run(conn, work, attempts=5):
    for attempt in range(attempts):
        try:
            with conn.transaction():
                return work(conn)
        except psycopg.errors.SerializationFailure:
            continue  # 40001: retry the whole transaction
    raise RuntimeError("still conflicting after retries")
```

### 40003: the commit outcome is unknown

`40003` means the commit may or may not have happened, for example because a bucket write timed out or a node stopped mid-commit. **Do not retry yet.** The error carries the transaction ID. Ask any node:

```sh
curl --cacert ca/ca.crt -H "Authorization: Bearer $TOKEN" \
  https://n1.example.net:7101/db/app/txn/<txid>
```

| `status` | Meaning |
|---|---|
| `committed` | Final. Do not run it again |
| `aborted` | It did not happen. You may run the operation again, as a new transaction |
| `unknown`, `quarantined`, or HTTP 503 `lookup_failed` | Not resolved. Repeat the lookup. Do not replay the operation |

Only `aborted` permits a new attempt. An unknown outcome is not a confirmed rollback.

## Read-only transactions

`BEGIN READ ONLY` gives a consistent read-only image of the whole database while writers continue. A write inside it returns `25006`.

## Things that differ from PostgreSQL

- **Any error inside `BEGIN … COMMIT` aborts the block.** Later statements return `25P02` until you send `ROLLBACK`.
- **No savepoints.** `ON CONFLICT ABORT`, `FAIL` and `ROLLBACK` discard the whole transaction, not one statement.
- **`CREATE TABLE` is not transactional.** It is one standalone statement and commits at once.
- **A multi-statement simple query is one implicit transaction.**

## Coming

- **Server-side retry of standalone statements and SDK helpers** for `40001` and `40003`.
- **Idempotency keys** recorded in the same commit, for exactly-once writes inside a published window.

---
Source: https://gauntlet-db.stavros.xyz/docs/concepts/transactions
