docs/docs/00200-core-concepts/00100-databases/00100-transactions-atomicity.md
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import { CppModuleVersionNotice } from "@site/src/components/CppModuleVersionNotice";
SpacetimeDB provides strong transactional guarantees for all database operations. Every reducer runs inside a database transaction, ensuring your data remains consistent and reliable even under concurrent load.
A database transaction is a sequence of operations that execute as a single, indivisible unit of work. In SpacetimeDB, each reducer invocation is a transaction - either all of its changes succeed and are committed to the database, or all changes are rolled back as if the reducer never ran.
SpacetimeDB transactions provide the following guarantees:
All-or-nothing execution: A reducer's changes either all succeed or all fail together. There's no partial state.
:::note This atomicity guarantee applies to reducers, which run in a single transaction. Procedures can manually open multiple separate transactions, where each transaction is individually atomic, but the procedure as a whole is not. See Procedures: Manual Transactions below. :::
Valid states only: Transactions ensure the database moves from one valid state to another. All constraints (unique keys, indexes, foreign key-like relationships in your logic) are enforced.
Consistent snapshots: Each reducer sees a consistent snapshot of the database and doesn't observe partial changes from other reducer executions.
This prevents race conditions and ensures predictable behavior.
Persistent changes: Once a reducer successfully commits, its changes are permanent and will survive server restarts. SpacetimeDB persists committed transactions to disk.
Every reducer invocation automatically runs in its own transaction:
You don't need to manually start or commit transactions in reducers - SpacetimeDB handles this automatically.
When a reducer calls another reducer directly (not via scheduling), they execute in the same transaction:
<Tabs groupId="server-language" queryString> <TabItem value="typescript" label="TypeScript">export const parent_reducer = spacetimedb.reducer((ctx) => {
TableA.insert({ /* ... */ });
// This runs in the SAME transaction
childReducer(ctx);
TableB.insert({ /* ... */ });
// All changes from both parent and child commit together
});
function childReducer(ctx) {
TableC.insert({ /* ... */ });
// If this throws, the parent's changes also roll back
if (someCondition) {
throw new Error('Child failed');
}
}
[SpacetimeDB.Reducer]
public static void ParentReducer(ReducerContext ctx)
{
ctx.Db.TableA.Insert(new RowA { /* ... */ });
// This runs in the SAME transaction
ChildReducer(ctx);
ctx.Db.TableB.Insert(new RowB { /* ... */ });
// All changes from both parent and child commit together
}
[SpacetimeDB.Reducer]
public static void ChildReducer(ReducerContext ctx)
{
ctx.Db.TableC.Insert(new RowC { /* ... */ });
// If this throws, the parent's changes also roll back
if (someCondition)
{
throw new Exception("Child failed");
}
}
#[reducer]
pub fn parent_reducer(ctx: &ReducerContext) -> Result<(), String> {
ctx.db.table_a().insert(RowA { /* ... */ });
// This runs in the SAME transaction
child_reducer(ctx)?;
ctx.db.table_b().insert(RowB { /* ... */ });
// All changes from both parent and child commit together
Ok(())
}
#[reducer]
pub fn child_reducer(ctx: &ReducerContext) -> Result<(), String> {
ctx.db.table_c().insert(RowC { /* ... */ });
// If this returns Err, the parent's changes also roll back
if some_condition {
return Err("Child failed".to_string());
}
Ok(())
}
using namespace SpacetimeDB;
// Forward declare child reducer to allow calling it before its definition
ReducerResult child_reducer(ReducerContext&, bool some_condition);
SPACETIMEDB_REDUCER(parent_reducer, ReducerContext ctx, bool some_condition) {
ctx.db[table_a].insert(RowA{ /* ... */ });
// This runs in the SAME transaction
ReducerResult result = child_reducer(ctx, some_condition);
if (result.is_err()) {
return result;
}
ctx.db[table_b].insert(RowB{ /* ... */ });
// All changes from both parent and child commit together
return Ok();
}
SPACETIMEDB_REDUCER(child_reducer, ReducerContext ctx, bool some_condition) {
ctx.db[table_c].insert(RowC{ /* ... */ });
// If this returns Err, the parent's changes also roll back
if (some_condition) {
return Err("Child failed");
}
return Ok();
}
:::important SpacetimeDB does not support nested transactions. Nested reducer calls execute in the same transaction as their parent. If you need separate transactions, use scheduled reducers instead. :::
Unlike reducers, procedures don't automatically run in transactions. Procedures can run transactions, but must manually open them using with_tx (Rust) or withTx (TypeScript). This gives procedures more flexibility:
with_tx/withTx call creates a new transaction that commits independentlySee Procedures for more details on manual transaction management.
Result<(), String> in Rust or throw exceptions with clear messagesSpacetimeDB does not support nested transactions. When one reducer calls another, they share the same transaction. If you need separate transactions, use scheduled reducers to trigger the second reducer asynchronously.
The #[auto_inc] sequence generator is not transactional: