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
Neon time travel lists blob versions on Azure and refuses the response when any version reports the literal version id \"null\". On Azure, a null version id means blob versioning is disabled on the storage account (only an unversioned base blob exists) or the blob predates versioning being enabled. Time travel needs a real version id for every version, so it aborts the whole listing.
Source
Thrown at libs/remote_storage/src/azure_blob.rs:988
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 Version { key, .. } = &vd;
let version_id = vd.version_id().map(|v| v.0.as_str());
if version_id == Some("null") {
return Err(TimeTravelError::Other(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} kind={:?}", vd.kind);
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_name(key);
if last_vd.last_modified > done_if_after {
tracing::debug!("Key {key} has version later than done_if_after, skipping");
continue;View on GitHub (pinned to 8f60b04da4)
Solutions
- Enable blob versioning on the storage account (Azure Portal: Data protection > Versioning, or az storage account blob-service-properties update --enable-versioning true)
- Use an account/container that has had versioning enabled since before any data was written
- Rewrite/migrate affected objects so fresh, versioned copies exist
- If time travel is not required for these paths, route them to a non-time-travel listing path
Defensive patterns
Strategy: validation
Validate before calling
// Before using time travel, confirm blob versioning is enabled on the storage account.
// CLI equivalent: az storage account blob-service-properties show --account-name <acct>
// (look for "isVersioningEnabled": true)
async fn assert_azure_versioning(account: &str) -> anyhow::Result<()> {
let props = az_cli_json(["storage", "account", "blob-service-properties", "show", "--account-name", account]).await?;
let enabled = props.pointer("/isVersioningEnabled").and_then(|v| v.as_bool()).unwrap_or(false);
anyhow::ensure!(enabled, "blob versioning disabled on account {account}; time travel requires it");
Ok(())
} Try / catch
// Detect the condition and fail fast with an actionable message.
let versions = match storage.list_versions(key, None, &cancel).await {
Ok(v) => v,
Err(TimeTravelError::Other(e)) if format!("{e:#}").contains("version_id='null'") => {
return Err(anyhow::anyhow!("bucket is not version-enabled (or holds pre-versioning blobs); enable versioning on the storage account"));
}
Err(e) => return Err(e.into()),
}; Prevention
- Enable blob versioning at account creation time, not after data lands
- Only write time-travel-relevant data to version-enabled containers
- Add a startup config check that verifies versioning before serving time-travel requests
When it happens
Trigger: Calling list_versions / time-travel APIs against an Azure storage account or container where versioning was never enabled, or which still contains blobs written before versioning was turned on.
Common situations: Pointing a time-travel-capable component at a plain storage account created without versioning; test/dev accounts; accounts where versioning was enabled after data was already written; manual uploads done outside the versioned pipeline.
Related errors
- max_keys_per_list_response can't be 0
- Failed to upload all blocks {:#?}
- Received abort for copy from {from} to {to}.
- Received failure response for copy from {from} to {to}.
- Received ListVersions response for key={key} with version_id
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/2da1a55f0cd9f053.
Report an issue: GitHub.