# SpacetimeDB Transactions and Consistency: A Complete Guide to ACID Guarantees in the Source Code

> Explore SpacetimeDB transactions and consistency. Understand ACID guarantees and how SpacetimeDB ensures strong transactional integrity for your data.

- Repository: [Clockwork Labs/SpacetimeDB](https://github.com/clockworklabs/SpacetimeDB)
- Tags: deep-dive
- Published: 2026-03-09

---

**SpacetimeDB guarantees strong ACID transactional consistency by automatically wrapping every reducer invocation in an implicit Serializable transaction while exposing explicit transaction controls through the SDK for complex procedures.**

SpacetimeDB treats transactional integrity as a core architectural pillar, ensuring that every database operation maintains atomicity, consistency, isolation, and durability. Understanding how SpacetimeDB transactions and consistency are implemented at the source code level helps developers write reliable applications without manually managing commit and rollback logic. This guide examines the concrete implementation in the `clockworklabs/SpacetimeDB` repository, from the trait definitions in [`crates/datastore/src/traits.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/datastore/src/traits.rs) to the locking mechanisms that enforce Serializable isolation.

## Transaction Architecture and Core Traits

The SpacetimeDB engine defines transaction behavior through two fundamental traits that separate read-only and mutable operations. These abstractions allow the system to enforce strict consistency while providing both automatic and manual transaction management.

### The Tx and MutTx Traits

The transaction interface resides in [`crates/datastore/src/traits.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/datastore/src/traits.rs), where the `Tx` and `MutTx` traits establish the contract for database operations. The `MutTx` trait specifically handles mutable transactions through three critical methods:

```rust
pub trait MutTx {
    type MutTx;
    fn begin_mut_tx(&self, isolation_level: IsolationLevel, workload: Workload) -> Self::MutTx;
    fn commit_mut_tx(&self, tx: Self::MutTx) -> Result<Option<(TxOffset, TxData, TxMetrics, Option<ReducerName>)>>;
    fn rollback_mut_tx(&self, tx: Self::MutTx) -> (TxOffset, TxMetrics, Option<ReducerName>);
}

```

In [`crates/datastore/src/locking_tx_datastore/datastore.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/datastore/src/locking_tx_datastore/datastore.rs), the concrete implementation of `begin_mut_tx` acquires a write lock on the committed state (lines 19-28), ensuring exclusive access for the transaction duration. The `commit_mut_tx` method (lines 54-58) writes the in-memory state and returns a `TxData` structure containing all row-level mutations.

### Isolation Levels and Serializable Execution

The `IsolationLevel` enum in [`crates/datastore/src/traits.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/datastore/src/traits.rs) defines five isolation tiers ranging from `ReadUncommitted` to `Serializable`. However, the current `Locking` implementation in [`datastore.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/datastore.rs) always operates at **Serializable** isolation—the strongest level—effectively ignoring the isolation parameter because the engine locks the entire committed state for the duration of the mutable transaction (lines 26-28).

This design choice guarantees that transactions see a stable snapshot of the database and that concurrent transactions cannot observe intermediate states or create race conditions.

## How SpacetimeDB Enforces Consistency

Consistency enforcement in SpacetimeDB operates through the `TxData` structure and automatic constraint validation. As documented in [`docs/docs/00200-core-concepts/00100-databases/00100-transactions-atomicity.md`](https://github.com/clockworklabs/SpacetimeDB/blob/main/docs/docs/00200-core-concepts/00100-databases/00100-transactions-atomicity.md), the engine ensures that only valid states become visible by aborting transactions that violate constraints.

### The TxData Structure

All row-level mutations—including inserts, deletes, and table truncations—accumulate in the `TxData` structure during a transaction's lifetime. When `commit_mut_tx` succeeds, the system hands `TxData` to the durability layer for persistence. If any constraint violation occurs during the commit process, the entire transaction aborts, guaranteeing that the database never enters an invalid state.

This mechanism ensures **atomicity**: a reducer either persists all its changes or rolls them back completely. No partial writes ever reach the committed state.

## Working with Transactions in Practice

SpacetimeDB provides two distinct models for transaction management: implicit transactions for reducers and explicit transactions for procedures requiring fine-grained control.

### Automatic Reducer Transactions

Every reducer invocation automatically runs inside an implicit transaction. The runtime invokes `begin_mut_tx` at the start of each reducer call and commits or rolls back based on the return value:

```rust
#[reducer]
pub fn add_user(ctx: &ReducerContext) -> Result<(), String> {
    // All statements belong to a single transaction.
    ctx.db.users().insert(User { id: 1, name: "Alice".into() });
    ctx.db.accounts().insert(Account { user_id: 1, balance: 0 })?;
    Ok(()) // → automatic commit
}

```

If `ctx.db.accounts().insert` fails due to a unique-key violation, the reducer returns an error and **nothing** persists—the transaction rolls back automatically. This eliminates the need for manual transaction management in standard business logic.

### Explicit Transactions in Procedures

For multi-step workflows, procedures expose the SDK's transaction API directly:

```rust
use spacetimedb::client::{SpacetimeDBClient, Tx};

pub fn transfer_funds(client: &SpacetimeDBClient, from: u64, to: u64, amount: i64) -> Result<(), String> {
    // Open a mutable transaction at Serializable isolation
    let mut tx = client.begin_mut_tx(spacetimedb::IsolationLevel::Serializable)?;
    
    // Perform reads/writes
    let from_balance = client.read_balance(&tx, from)?;
    if from_balance < amount {
        client.rollback_mut_tx(tx)?;
        return Err("Insufficient funds".into());
    }
    
    client.update_balance(&mut tx, from, from_balance - amount)?;
    let to_balance = client.read_balance(&tx, to)?;
    client.update_balance(&mut tx, to, to_balance + amount)?;
    
    // Commit ensures all writes succeed or none do
    client.commit_mut_tx(tx)?;
    Ok(())
}

```

The SDK translates these calls into the same `begin_mut_tx` and `commit_mut_tx` flow used by the runtime, maintaining identical ACID guarantees.

### Nesting and Scheduling Behavior

**Nested reducer calls** execute within the *same* transaction context; SpacetimeDB does not support true nested transactions. This means that if an inner reducer fails, the entire outer transaction rolls back.

Conversely, **scheduled reducers** (`#[spacetimedb::reducer]` with scheduling) start fresh transactions, allowing deliberate separation of logical units of work. This distinction is crucial for designing complex workflows that require partial failure isolation versus atomic batch operations.

## Implementation Deep Dive: Locking and Durability

The `Locking` datastore implementation in [`crates/datastore/src/locking_tx_datastore/datastore.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/datastore/src/locking_tx_datastore/datastore.rs) enforces Serializable isolation by acquiring a write lock on the `CommittedState` at transaction start. This coarse-grained locking strategy prevents phantom reads and write skew by ensuring only one mutable transaction accesses the committed state at a time.

Once `commit_mut_tx` completes, the durability layer (implemented in [`crates/durability/src/imp/local.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/durability/src/imp/local.rs)) writes the transaction to an on-disk commit log. This separation of concerns—transaction logic in the datastore and persistence in the durability layer—ensures that committed changes survive process or machine restarts while maintaining high performance for in-memory operations.

## Summary

- **Every reducer is an implicit transaction** that automatically commits on success or rolls back on failure, eliminating boilerplate transaction code.
- **Serializable isolation** is enforced by default through coarse-grained locking of the committed state, preventing all concurrency anomalies.
- **Consistency guarantees** are maintained by validating constraints during commit; any violation aborts the entire transaction via the `TxData` rollback mechanism.
- **Explicit transaction control** is available through the SDK for procedures, using `begin_mut_tx`, `commit_mut_tx`, and `rollback_mut_tx` with the same ACID guarantees.
- **Durability** is achieved through a separate commit-log layer that persists `TxData` after successful commits, ensuring data survives restarts.

## Frequently Asked Questions

### What isolation level does SpacetimeDB use?

SpacetimeDB currently operates exclusively at **Serializable** isolation, the strongest level defined in the SQL standard. While the `IsolationLevel` enum in [`crates/datastore/src/traits.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/datastore/src/traits.rs) defines multiple levels (ReadUncommitted through Serializable), the `Locking` implementation in [`datastore.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/datastore.rs) ignores the isolation parameter and always locks the entire committed state during mutable transactions.

### How do I manually control transactions in SpacetimeDB?

Procedures can open explicit transactions using the SDK's `begin_mut_tx`, `commit_mut_tx`, and `rollback_mut_tx` methods. This pattern is essential for multi-step workflows where you need to check intermediate conditions before committing, such as validating account balances before transferring funds between tables.

### What happens if a reducer fails halfway through?

If a reducer panics or returns an `Err`, SpacetimeDB automatically rolls back the entire implicit transaction. Because all operations within the reducer scope write to a staging `TxData` structure rather than the committed state, partial writes never become visible to other transactions or persist to disk.

### Do nested reducers run in separate transactions?

No. Nested reducer calls execute within the **same** transaction context as the parent reducer. SpacetimeDB does not implement true nested transactions (savepoints), meaning a failure in an inner reducer causes the entire outer transaction to roll back. Use scheduled reducers if you need separate transaction boundaries.