tursodatabase/turso · error

write-write conflict (transaction rolled back)

Error message

write-write conflict (transaction rolled back)

What it means

Reported by the MVCC concurrent-transaction REPL (tursodb --mvcc) when a statement fails with turso_core::LimboError::WriteWriteConflict. Experimental MVCC mode uses optimistic snapshot isolation: each transaction validates its writes against concurrently committed changes. When another connection already committed writes to the same rows/pages since this transaction's snapshot was taken, the engine rolls the losing transaction back and the REPL prints 'ERROR: write-write conflict (transaction rolled back)'.

Source

Thrown at cli/mvcc_repl.rs:142

}

fn execute_and_display(conn: &Arc<Connection>, sql: &str, conn_name: &str) -> anyhow::Result<()> {
    let mut stmt = conn.prepare(sql).map_err(|e| anyhow::anyhow!("{}", e))?;

    match stmt.run_collect_rows() {
        Ok(rows) => {
            if rows.is_empty() {
                println!("[{conn_name}] OK");
            } else {
                for row in rows {
                    let formatted: Vec<String> = row.iter().map(fmt_value).collect();
                    println!("[{conn_name}] {}", formatted.join(" | "));
                }
            }
            Ok(())
        }
        Err(LimboError::WriteWriteConflict) => Err(anyhow::anyhow!(
            "write-write conflict (transaction rolled back)"
        )),
        Err(e) => Err(anyhow::anyhow!("{e}")),
    }
}

fn fmt_value(v: &Value) -> String {
    use turso_core::Numeric;
    match v {
        Value::Null => "NULL".to_string(),
        Value::Numeric(Numeric::Integer(i)) => i.to_string(),
        Value::Numeric(Numeric::Float(f)) => {
            let fval: f64 = (*f).into();
            // Format floats without trailing zeros for cleaner display
            if fval.fract() == 0.0 && fval.abs() < 1e10 {
                format!("{fval:.1}")
            } else {
                format!("{fval}")
            }

View on GitHub (pinned to 0e69fa4af1)

Solutions

  1. Accept the rollback and retry: the losing transaction is already rolled back, so re-run BEGIN CONCURRENT plus the write statements on the failed connection
  2. Keep transactions short so fewer concurrent snapshots overlap on the same rows
  3. Route hot-row writes through a single connection or an application-level lock so commits never interleave
  4. Re-read data after the other connection commits so the retried transaction starts from a fresh snapshot

Example fix

// before (one-shot, fails on conflict):
mvcc> conn2 INSERT INTO t VALUES (42)
[conn2] ERROR: write-write conflict (transaction rolled back)

// after (rollback + retry):
mvcc> conn2 ROLLBACK
mvcc> conn2 BEGIN CONCURRENT
mvcc> conn2 INSERT INTO t VALUES (42)
Defensive patterns

Strategy: retry

Type guard

fn is_write_write_conflict(err: &turso_core::LimboError) -> bool {
    matches!(err, turso_core::LimboError::WriteWriteConflict)
}

Try / catch

loop {
    conn.execute("BEGIN CONCURRENT")?;
    match run_writes(conn) {
        Ok(_) => { conn.execute("COMMIT")?; break; }
        Err(e) if is_write_write_conflict(&e) => {
            let _ = conn.execute("ROLLBACK"); // already rolled back; keep for hygiene
            continue;                       // optionally with backoff
        }
        Err(e) => return Err(e),
    }
}

Prevention

When it happens

Trigger: In the REPL, run 'conn1 BEGIN CONCURRENT' and a write (INSERT/UPDATE/DELETE) on conn1, then on conn2 (also in a transaction) write to the same table/rows and let either side commit first; the other connection's next write statement or commit returns WriteWriteConflict via stmt.run_collect_rows().

Common situations: Interactive multi-connection conflict testing (the module doc shows exactly this conn1/conn2 INSERT scenario); scripts that assume lock-based blocking writes instead of optimistic validation; long-running read-then-write transactions whose snapshots go stale while another session commits.

Related errors


AI-assisted analysis of tursodatabase/turso@0e69fa4af1 (2026-08-20). Data as JSON: /api/errors/663d5f76a11532ab. Report an issue: GitHub.