tursodatabase/turso · critical

spill_append_frames_to_wal requires a WAL

Error message

spill_append_frames_to_wal requires a WAL

What it means

Pager::spill_append_frames_to_wal() appends the prepared spill pages as WAL frames and requires self.wal with expect("spill_append_frames_to_wal requires a WAL"). Callers are supposed to route through the PreparingWal* states first, which initialize the WAL; reaching this function with no WAL attached breaks that ordering.

Source

Thrown at core/storage/pager.rs:4275

                        "try_spill_dirty_pages: successfully spilled {} pages to disk",
                        pages.len()
                    );
                    return Ok(IOResult::Done(()));
                }
            }
        }
    }

    /// Append the prepared spill `pages` as WAL frames. Returns `Done` if
    /// the write completed synchronously, otherwise transitions to
    /// `SpillState::WritingToWal` and yields the write completion. The WAL
    /// must already be initialized (callers route through `PreparingWal*`
    /// first).
    fn spill_append_frames_to_wal(&self, pages: Vec<PinGuard>) -> IOResultOr<()> {
        let wal = self
            .wal
            .as_ref()
            .expect("spill_append_frames_to_wal requires a WAL");
        let page_sz = self.get_page_size().unwrap_or_default();
        let wal_pages: Vec<PageRef> = pages
            .iter()
            .map(|p| -> Result<PageRef> {
                self.subjournal_page_if_required(p)?;
                // Set write_pending on all pages before WAL write so callback can
                // detect mid-write modifications.
                p.set_write_pending();
                Ok(p.to_page())
            })
            .collect::<Result<Vec<_>>>()?;
        let c = wal.append_frames_vectored(wal_pages, page_sz)?;

        if c.succeeded() {
            // Synchronous completion, WAL tags already set by callback.
            {
                let mut cache = self.page_cache.write();
                for page in &pages {

View on GitHub (pinned to 492c4a71cd)

Solutions

  1. Report to Turso with the workload - the spill state machine reached append without WAL init
  2. Avoid overwhelming the page cache on WAL-less (e.g. in-memory) databases: raise cache size or batch commits
  3. Disable cache spill on such databases (pager.set_spill_enabled(false))
  4. Ensure the connection is not being closed concurrently with a long write statement
Defensive patterns

Strategy: validation

Validate before calling

// Same guard as the spill-entry point: ensure WAL is in play or spill is off
// before heavy writes on WAL-less databases:
pager.set_spill_enabled(false); // embedder code for :memory: databases
// Application level: keep single-statement write volume under the cache size,
// or use journal_mode=WAL for file databases.

Try / catch

let result = std::panic::catch_unwind(AssertUnwindSafe(|| do_big_insert(&conn)));
if result.is_err() { conn.close().ok(); /* reopen, split the write into batches */ }

Prevention

When it happens

Trigger: A cache spill that skips or fails through the PreparingWalStart/PreparingWalFinish initialization states and lands directly in frame append on a pager whose WAL is absent (in-memory database, WAL-less configuration) or was torn down mid-spill.

Common situations: Memory databases under heavy write load, engine regressions in spill state ordering, WAL teardown racing a spill (close during spill).

Related errors


AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-08-20). Data as JSON: /api/errors/afb27e85718e4bae. Report an issue: GitHub.