nautechsystems/nautilus_trader · error · anyhow::Error

Failed to write block data: {e}

Error message

Failed to write block data: {e}

What it means

write_block_binary serializes one Block into PostgreSQL COPY BINARY row format and sends it with PgCopyIn::send; a send failure is wrapped as 'Failed to write block data'. Because the error propagates via `?`, the whole copy_blocks batch aborts. This is a mid-stream write failure — transport error or the server having already rejected the COPY session.

Source

Thrown at crates/adapters/blockchain/src/cache/copy.rs:432

            let l1_gas_used_bytes = (l1_gas_used as i64).to_be_bytes();
            row_data.write_all(&(l1_gas_used_bytes.len() as i32).to_be_bytes())?;
            row_data.write_all(&l1_gas_used_bytes)?;
        } else {
            row_data.write_all(&(-1i32).to_be_bytes())?; // NULL value
        }

        if let Some(l1_fee_scalar) = block.l1_fee_scalar {
            let l1_fee_scalar_bytes = (l1_fee_scalar as i64).to_be_bytes();
            row_data.write_all(&(l1_fee_scalar_bytes.len() as i32).to_be_bytes())?;
            row_data.write_all(&l1_fee_scalar_bytes)?;
        } else {
            row_data.write_all(&(-1i32).to_be_bytes())?; // NULL value
        }

        copy_in
            .send(row_data)
            .await
            .map_err(|e| anyhow::anyhow!("Failed to write block data: {e}"))?;
        Ok(())
    }

    /// Writes a single pool swap in PostgreSQL binary format.
    ///
    /// Each row in binary format consists of:
    /// - 2-byte field count
    /// - For each field: 4-byte length followed by data (or -1 for NULL)
    async fn write_pool_swap_binary(
        &self,
        copy_in: &mut sqlx::postgres::PgCopyIn<sqlx::pool::PoolConnection<sqlx::Postgres>>,
        chain_id: u32,
        swap: &PoolSwap,
    ) -> anyhow::Result<()> {
        use std::io::Write;
        let mut row_data = Vec::new();

        row_data.write_all(&19u16.to_be_bytes())?;

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Retry copy_blocks with a smaller batch size to isolate the failing block and survive transient drops
  2. Compare the fields written by write_block_binary to the COPY column list for block after recent migrations
  3. Check DB server logs for why the COPY session aborted
  4. Enable connection health checks (test_before_acquire) and re-run the backfill

Example fix

// before
handler.copy_blocks(chain_id, &all_blocks).await?;
// after
for chunk in all_blocks.chunks(500) {
    handler.copy_blocks(chain_id, chunk).await?;
}
Defensive patterns

Strategy: retry

Try / catch

let result = handler.copy_blocks(chain_id, &blocks).await;
match result {
    Err(e) if e.to_string().contains("Failed to write block data") => {
        tokio::time::sleep(Duration::from_secs(5)).await;
        handler.copy_blocks(chain_id, &blocks).await?; // retry whole batch; COPY is atomic
    }
    other => other?,
}

Prevention

When it happens

Trigger: Connection loss while streaming a large block batch, server abort of the COPY (e.g. previous row triggered an error surfaced on the next send), or serialization code writing bytes inconsistent with the declared column list.

Common situations: Multi-thousand-block backfills across an unreliable network/VPN; DB restart mid-backfill; after a migration changed the block table but write_block_binary still writes the old field layout.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/330f9c2a362bf316. Report an issue: GitHub.