tursodatabase/turso · critical
page evicted between scan and prepare
Error message
page evicted between scan and prepare
What it means
Commit preparation walks commit_info.page_sources; entries recorded as PageSource::Cached(page_id) are re-fetched from the page cache with expect("page evicted between scan and prepare"), relying on pages being pinned between the commit scan and frame preparation. The panic fires when such a page is no longer in the cache - an eviction raced the commit.
Source
Thrown at core/storage/pager.rs:4552
let page_sz = self.get_page_size_unchecked();
let mut commit_info = self.commit_info.write();
let mut cache = self.page_cache.write();
'inner: loop {
let cursor = commit_info.page_source_cursor;
if cursor >= commit_info.page_sources.len() {
break 'inner;
}
let total = commit_info.page_sources.len();
let is_last = cursor + 1 >= total;
// Linear consumption, no lookup required
let page = match &commit_info.page_sources[cursor] {
PageSource::Cached(page_id) => {
let page_key = PageCacheKey::new(*page_id);
cache
.get(&page_key)?
.expect("page evicted between scan and prepare")
}
PageSource::Evicted(page) => page.clone(),
};
// Defensive check: prepare_frames will read page contents,
// which panics if the buffer is not loaded. If we got here
// with an unloaded page (e.g. an evicted dirty page whose
// backing WAL frame was truncated by a savepoint rollback),
// surface an internal error instead of panicking.
if !page.is_loaded() {
return Err(LimboError::InternalError(format!(
"dirty page {} has no buffer loaded at commit time",
page.get().id()
))
.into());
}
turso_assert!(
page.get().overflow_cells.is_empty(),
"dirty page still has overflow cells at commit time",View on GitHub (pinned to 492c4a71cd)
Solutions
- Report to Turso with a reproduction - pin-discipline bug, not caller-fixable
- Raise the page cache size so the commit's dirty set fits without eviction pressure
- Commit in smaller batches to shrink the page_sources list
- Run PRAGMA integrity_check after any occurrence to confirm the database is intact
Defensive patterns
Strategy: try-catch
Validate before calling
// Application-level: keep the per-transaction dirty set comfortably below the
// page cache so the commit scan cannot lose pages to eviction:
// - raise cache size (PRAGMA cache_size / engine config), or
// - commit in smaller batches.
conn.execute("PRAGMA cache_size = -262144", ())?; // example: ~256MB cache Try / catch
let result = std::panic::catch_unwind(AssertUnwindSafe(|| conn.execute("COMMIT", ())));
if result.is_err() {
conn.close().ok();
// Uncommitted work is lost by design; reopen and verify:
// PRAGMA integrity_check;
} Prevention
- Size the page cache above your largest transaction's dirty set
- Split very large transactions into batches with intermediate commits
- Avoid concurrent statement churn during COMMIT of big transactions
- Run integrity_check after any commit-path panic before reusing the file
When it happens
Trigger: Committing a large dirty set while cache pressure evicts a page that was recorded as cached; or pin-count bookkeeping dropped a pin early (a statement released pages mid-commit). The neighboring unloaded-page check converts the milder variant into an InternalError, but the cache miss itself panics here.
Common situations: Big transactions near the cache limit, concurrent statements finishing during commit, engine regressions in pin discipline.
Related errors
- buffer not loaded
- DB should not be initialized and should not do any IO
- Page size too small, a ptrmap page cannot map any db pages.
- subjournal must be opened
- invalid page size stored
AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-08-20).
Data as JSON: /api/errors/8ecb90550d6a0731.
Report an issue: GitHub.