FuelLabs/fuel-core · error · StorageError

Transaction status already exists for tx {}

Error message

Transaction status already exists for tx {}

What it means

While indexing an imported block, the worker writes each transaction's status with db.update_tx_status(id, status). The method returns the previously stored status; a Some return means this tx id already had a status recorded, i.e., the block/tx is being processed twice. The worker treats duplicate status writes as an inconsistency and aborts indexing instead of silently overwriting history.

Source

Thrown at crates/fuel-core/src/graphql_api/worker_service.rs:461

    }

    Ok(())
}

fn persist_transaction_status<T>(
    import_result: &ImportResult,
    asset_metadata_indexation_enabled: bool,
    db: &mut T,
) -> StorageResult<()>
where
    T: OffChainDatabaseTransaction,
{
    for TransactionExecutionStatus { id, result } in import_result.tx_status.iter() {
        let status =
            from_executor_to_status(&import_result.sealed_block.entity, result.clone());

        if db.update_tx_status(id, status)?.is_some() {
            return Err(anyhow::anyhow!(
                "Transaction status already exists for tx {}",
                id
            )
            .into());
        }

        let TransactionExecutionResult::Success { receipts, .. } = result else {
            continue
        };

        update_receipt_based_indexation(receipts, db, asset_metadata_indexation_enabled)?;
    }
    Ok(())
}

pub fn process_transactions<'a, I, T>(transactions: I, db: &mut T) -> StorageResult<()>
where
    I: Iterator<Item = &'a Transaction>,

View on GitHub (pinned to b9d4d170da)

Solutions

  1. Do not re-run the off-chain worker over blocks already indexed; resume from the off-chain DB's recorded height.
  2. If a full re-index is intended, wipe the off-chain database first so no prior statuses exist.
  3. Inspect import_result.tx_status for duplicate tx ids before indexing; deduplicate if the producer can emit repeats.
  4. Verify the off-chain DB snapshot matches the on-chain DB height before restarting workers.
Defensive patterns

Strategy: validation

Validate before calling

// Skip txs already indexed before writing statuses during re-processing.
let ids: HashSet<_> = import_result.tx_status.iter().map(|s| s.id).collect();
if ids.len() != import_result.tx_status.len() {
    anyhow::bail!("import result contains duplicate tx statuses");
}
for TransactionExecutionStatus { id, result } in import_result.tx_status.iter() {
    if db.update_tx_status(id, placeholder)?.is_some() {
        anyhow::bail!("tx {} already indexed; block processed twice?", id);
    }
}

Try / catch

match update_block_metadata(&import_result, flag, &mut db) {
    Ok(()) => Ok(()),
    Err(e) if e.to_string().contains("status already exists") => {
        // duplicate processing: verify heights, and for intentional re-index wipe the off-chain DB first
        anyhow::bail!("duplicate import detected: {}", e)
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: The same TransactionExecutionStatus appears in an import result for a tx already indexed — duplicate block import, overlapping import results, or re-running the off-chain worker over an already-processed block.

Common situations: Re-importing a block after a crash where part of the off-chain transaction committed; worker replay without resetting the off-chain DB; bugs producing duplicate entries in import_result.tx_status; restoring off-chain DBs from mixed snapshots.

Related errors


AI-assisted analysis of FuelLabs/fuel-core@b9d4d170da (2026-08-16). Data as JSON: /api/errors/adc30d901d4b0af5. Report an issue: GitHub.