How Turso's Transaction State Machine Handles Isolation Levels

Turso's transaction state machine enforces snapshot isolation as its sole MVCC isolation level by assigning immutable begin timestamps to transactions, validating reads against multiversion chains, and detecting write-write conflicts eagerly before commit.

Turso is an edge-hosted distributed database that extends SQLite with a multiversion concurrency-control (MVCC) storage engine. While traditional SQL databases offer configurable isolation levels like READ COMMITTED or SERIALIZABLE, Turso's architecture simplifies concurrency control by implementing snapshot isolation exclusively for all MVCC-enabled transactions. The transaction state machine—comprising the Transaction and CommitStateMachine types—orchestrates this behavior through deterministic state transitions in core/mvcc/database/mod.rs.

Snapshot Isolation: The Only MVCC Level

Turso does not expose READ COMMITTED, REPEATABLE READ, or SERIALIZABLE as user-selectable isolation modes. Instead, every transaction initiated with BEGIN CONCURRENT operates under snapshot isolation semantics, backed by logical timestamp ordering rather than locking.

The TransactionState Enum

At the core of the state machine is the TransactionState enum. This tracks the transaction lifecycle through discrete phases:

enum TransactionState {
    Active,                     // Initial state with assigned begin_ts
    Preparing(u64),             // Commit in progress, end_ts allocated
    Committed(u64),             // Final timestamp, visible globally
    Aborted,
    Terminated,
}

When a client executes BEGIN CONCURRENT, the system creates a Transaction instance in the Active state with a begin_ts drawn from the MvccClock defined in core/mvcc/mod.rs.

Enforcing Snapshot Isolation Properties

The state machine guarantees snapshot isolation through three coordinated mechanisms: immutable read snapshots, version visibility control, and eager conflict detection.

Consistent Snapshot Reads

When a transaction starts, it captures a begin timestamp (begin_ts) from the logical clock. All subsequent reads via MvStore::read inspect the version chain and return a row only if the transaction's begin_ts falls between the version's begin and end timestamps. This snapshot remains immutable for the transaction's lifetime.

In core/mvcc/database/mod.rs, the visibility check uses the TxTimestampOrID type to distinguish between committed timestamps and active transaction IDs:

// Simplified visibility logic from MvStore::read
if version.begin_ts <= tx.begin_ts && version.end_ts > tx.begin_ts {
    return Ok(Some(version)); // Visible in snapshot
}

Preventing Dirty Writes

Write operations remain invisible to other transactions until commit completes. When a transaction modifies a row, MvStore::upsert, insert, or delete assigns the writer's TxID (rather than a timestamp) to the new version's begin field. The read path automatically filters out any version whose begin value is a TxID, effectively blocking dirty reads while allowing the writer to see its own uncommitted changes.

Eager Write-Write Conflict Detection

Turso detects conflicts immediately rather than at commit time. Before writing, the system checks if the target row has an uncommitted version via row_has_uncommitted_version_for_tx. If another active transaction has modified the row, the operation returns LimboError::WriteWriteConflict:

// From core/mvcc/database/mod.rs
if self.row_has_uncommitted_version_for_tx(&key, tx_id) {
    return Err(LimboError::WriteWriteConflict);
}

This prevents serialization anomalies (G0 cycles) and eliminates the need for blocking locks during the commit phase.

The Commit State Machine

When COMMIT executes, the transaction transitions from Active to Preparing(end_ts), where end_ts is allocated from the MvccClock. The CommitStateMachine in core/mvcc/database/mod.rs then executes a deterministic sequence: Initial → Commit → WaitForDependencies → BuildLogRecord → RewriteLiveVersions → FinalizeCommit.

During the RewriteLiveVersions phase, temporary TxID markers in the version chain are atomically replaced with the final Timestamp, making the changes visible to transactions with higher begin_ts values.

Practical Transaction Lifecycle

The following example demonstrates how snapshot isolation behaves across concurrent connections:

// Transaction 1: Start snapshot-isolated transaction
let conn1 = db.connect();
conn1.execute("BEGIN CONCURRENT").unwrap(); // State: Active, begin_ts = 42
let rows = conn1.query("SELECT value FROM t WHERE id = 1", ()).unwrap();
assert_eq!(rows[0].get_int(0).unwrap(), 10); // Sees snapshot value

// Transaction 2: Attempt concurrent write to same row
let conn2 = db.connect();
conn2.execute("BEGIN CONCURRENT").unwrap();
let err = conn2.execute("UPDATE t SET value = 20 WHERE id = 1");
assert!(matches!(err, Err(LimboError::WriteWriteConflict))); // Eager conflict

// Transaction 1: Commit through state machine
conn1.execute("COMMIT").unwrap(); // Active → Preparing(43) → Committed(43)

// Transaction 3: Sees committed value with new snapshot
let conn3 = db.connect();
conn3.execute("BEGIN CONCURRENT").unwrap(); // begin_ts = 44
let rows = conn3.query("SELECT value FROM t WHERE id = 1", ()).unwrap();
assert_eq!(rows[0].get_int(0).unwrap(), 20); // Sees timestamped version

Fallback to SQLite Locking

Transactions started with plain BEGIN (without the CONCURRENT keyword) bypass the MVCC state machine entirely. These sessions use SQLite's native page-level locking, which behaves similarly to read committed isolation. Only transactions using BEGIN CONCURRENT participate in the snapshot isolation system.

Key Source Files

The implementation spans these critical paths in the tursodatabase/turso repository:

  • core/mvcc/database/mod.rs: Defines Transaction, TransactionState, CommitStateMachine, and the MVCC read/write paths that enforce snapshot isolation.
  • core/mvcc/database/hermitage_tests.rs: Comprehensive test suite validating snapshot isolation behavior including write skew and phantom prevention.
  • core/connection.rs: Entry point for BEGIN CONCURRENT command parsing and transaction initialization.
  • core/mvcc/mod.rs: Implements MvccClock, PackedTs, and TxTimestampOrID auxiliary types.

Summary

  • Turso's transaction state machine supports snapshot isolation exclusively for MVCC transactions; no other isolation levels are selectable.
  • The TransactionState enum tracks atomic progress from Active through Preparing to Committed.
  • Begin timestamps (begin_ts) create immutable read snapshots for the transaction duration using the MvccClock.
  • Write-write conflicts are detected eagerly via MvStore methods that return LimboError::WriteWriteConflict immediately.
  • The CommitStateMachine coordinates commit ordering through deterministic state transitions ending with RewriteLiveVersions.
  • Non-concurrent transactions fall back to SQLite's page-level locking without MVCC semantics.

Frequently Asked Questions

Does Turso support the SERIALIZABLE isolation level?

No, Turso intentionally does not implement SERIALIZABLE, READ COMMITTED, or REPEATABLE READ as distinct isolation levels. The MVCC subsystem in core/mvcc/ provides snapshot isolation exclusively, which prevents write skew and phantom reads without the performance overhead of strict serializable schedules.

What happens when two transactions try to modify the same row?

The second transaction receives an immediate LimboError::WriteWriteConflict error when executing the write statement. This eager conflict detection occurs inside MvStore::upsert and related methods, preventing lost updates without requiring row locking or blocking waits.

How does Turso handle read-only transactions?

Read-only transactions remain in the Active state for their entire session. Since they never modify data, they do not invoke the CommitStateMachine, but they still retain their initial begin_ts snapshot to guarantee repeatable reads against the version chain stored in the MVCC engine.

What is the difference between BEGIN and BEGIN CONCURRENT?

BEGIN uses SQLite's traditional page-level locking and behaves like read committed isolation, while BEGIN CONCURRENT initiates an MVCC transaction with snapshot isolation. Only CONCURRENT transactions participate in the version chain system, timestamp-based visibility checks, and the TransactionState machine.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →