neondatabase/neon · error · TimeTravelError
Received ListVersions response for key={key} with version_id
Error message
Received ListVersions response for key={key} with version_id='null', indicating either disabled versioning, or legacy objects with null version id values What it means
GCS port of the same time-travel guard as the Azure backend: while grouping listed versions per key, a version whose id is the literal string \"null\" aborts the listing. GCS identifies versions by numeric generations, so a \"null\" id signals a non-versioned/legacy situation — data imported with S3-style 'null' version markers or synthetic ids from migration tooling — which time travel cannot address.
Source
Thrown at libs/remote_storage/src/gcs_bucket.rs:1232
tracing::info!(
"Built list for time travel with {} versions and deletions",
versions_and_deletes.len()
);
// Work on the list of references instead of the objects directly,
// otherwise we get lifetime errors in the sort_by_key call below.
let mut versions_and_deletes = versions_and_deletes.iter().collect::<Vec<_>>();
versions_and_deletes.sort_by_key(|vd| (&vd.key, &vd.last_modified));
let mut vds_for_key = HashMap::<_, Vec<_>>::new();
for vd in &versions_and_deletes {
let GCSVersion { key, .. } = &vd;
if Some(vd.id.0.as_str()) == Some("null") {
// TODO: check the behavior of using the SDK on a non-versioned container
return Err(TimeTravelError::Other(anyhow::anyhow!(
"Received ListVersions response for key={key} with version_id='null', \
indicating either disabled versioning, or legacy objects with null version id values"
)));
}
tracing::trace!("Parsing version key={key} id={:?}", vd.id);
vds_for_key.entry(key).or_default().push(vd);
}
let warn_threshold = 3;
let max_retries = 10;
let is_permanent = |e: &_| matches!(e, TimeTravelError::Cancelled);
for (key, versions) in vds_for_key {
let last_vd = versions.last().unwrap();
let key = self.relative_path_to_gcs_object(key);
if last_vd.last_modified > done_if_after {
/// Case 1: we have a recent object outside of our restore window.
tracing::trace!("Key {key} has version later than done_if_after, skipping");
continue;View on GitHub (pinned to 8f60b04da4)
Solutions
- Identify the offending keys (the error names the key) and rewrite those objects through normal GCS writes so they carry real generations
- Re-run the migration/import step that produced 'null' version ids, dropping the null markers
- Verify listing tooling maps GCS generations directly (they are numeric strings) instead of S3 null-version semantics
- Route non-versioned legacy paths away from time-travel APIs
Defensive patterns
Strategy: validation
Validate before calling
// Before time travel, scan one page of versions for 'null' ids and fail with guidance.
async fn assert_gcs_generations(storage: &Arc<GenericRemoteStorage>, key: &RemotePath, cancel: &CancellationToken) -> anyhow::Result<()> {
let versions = storage.list_versions(key, Some(MaxKeys::new(100)), cancel).await
.map_err(|e| anyhow::anyhow!("listing failed: {e:#}"))?;
let null_ids = versions.versions.iter().filter(|v| v.id.0 == "null").count();
anyhow::ensure!(null_ids == 0,
"{null_ids} versions of {key} carry a 'null' id (migrated/legacy data); rewrite them via normal GCS writes");
Ok(())
} Try / catch
// Detect the family and report the remediation.
let versions = match storage.list_versions(key, None, &cancel).await {
Ok(v) => v,
Err(TimeTravelError::Other(e)) if format!("{e:#}").contains("version_id='null'") => {
anyhow::bail!("'null' version id found (legacy/migrated data without generations); rewrite affected objects before time travel");
}
Err(e) => return Err(e.into()),
}; Prevention
- Migrate S3-style data into GCS without carrying 'null' version markers — map generations directly
- Validate imported datasets with a listing sweep before enabling time travel on them
- Write all versioned data through the normal GCS write path so generations are always assigned
When it happens
Trigger: list_versions / time travel on a GCS bucket containing objects whose generation was recorded as the string \"null\" — typically data migrated from S3-style stores or written by tooling that emits 'null' version ids for non-versioned objects.
Common situations: Buckets populated by S3-to-GCS migration tools; test fixtures with hand-written version ids; time travel pointed at data written outside the normal GCS write path.
Related errors
- no 'updated' field
- Received ListVersions response for key={key} with version_id
- no items returned
- max keys reached
- GCS PUT error \n\t {:?}
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/27314601acd40f5f.
Report an issue: GitHub.