neondatabase/neon · warning · DownloadError
max keys reached
Error message
max keys reached
What it means
list_versions paginates until it has collected every version, but if the caller supplied max_keys it stops and errors as soon as the accumulated count reaches that limit. This is a deliberate guard refusing to return a silently-truncated result set: hitting it means at least max_keys versions exist and the listing was cut short.
Source
Thrown at libs/remote_storage/src/gcs_bucket.rs:304
)
)
}
Ok(
GCSVersion {
key: self.gcs_object_to_relative_path(&name),
last_modified: to_system_time(updated).unwrap(),
id: VersionId(generation.expect("no version id")),
time_deleted: to_system_time(time_deleted),
}
)
}).collect::<Result<Vec<GCSVersion>, _>>();
versions.versions.extend(version_listing?);
if let Some(max_keys) = max_keys {
if versions.versions.len() >= max_keys.get().try_into().unwrap() {
return Err(DownloadError::Other(
anyhow::anyhow!("max keys reached")
));
}
}
if continuation_token.is_none() {
break
}
}
Ok(versions)
}
async fn put_object(
&self,
byte_stream: impl Stream<Item = std::io::Result<Bytes>> + Send + Sync + 'static,
fs_size: usize,
to: &RemotePath,View on GitHub (pinned to 8f60b04da4)
Solutions
- Raise or remove the max_keys limit for the listing if the caller can handle the full result set
- Treat this error as the explicit 'too many versions' signal and paginate in max_keys-sized batches via continuation tokens
- Reduce version churn: compact or delete old versions so listings fit the limit
Example fix
// before: hard error once the limit is hit
if let Some(max_keys) = max_keys {
if versions.versions.len() >= max_keys.get().try_into().unwrap() {
return Err(DownloadError::Other(anyhow::anyhow!("max keys reached")));
}
}
// after: truncate explicitly and signal, rather than erroring
if let Some(max_keys) = max_keys {
let limit: usize = max_keys.get().try_into().unwrap();
if versions.versions.len() >= limit {
versions.versions.truncate(limit);
versions.truncated = true;
return Ok(versions);
}
} Defensive patterns
Strategy: validation
Validate before calling
// Size max_keys to the expected version count before calling (e.g. from prior metrics). const EXPECTED_VERSIONS_PER_KEY: usize = 1000; let max_keys = MaxKeys::new(EXPECTED_VERSIONS_PER_KEY.next_power_of_two()); // or omit max_keys entirely when the caller can stream the full set
Try / catch
// Treat 'max keys reached' as a truncation signal, not a crash.
let listing = match storage.list_versions(key, Some(max_keys), &cancel).await {
Ok(v) => v,
Err(DownloadError::Other(e)) if format!("{e:#}").contains("max keys reached") => {
tracing::warn!("listing for {key} hit max_keys={}; continuing with degraded history", max_keys.get());
// escalate to a paginated walk without the limit, or degrade gracefully
return handle_truncated(key).await;
}
Err(e) => return Err(e),
}; Prevention
- Pass max_keys only when you genuinely cannot handle the full result
- Budget the limit against real version churn (uploads + deletes per key per retention window)
- Compact or expire old versions so listings stay bounded
When it happens
Trigger: Calling a listing API with max_keys set while the bucket/prefix contains at least max_keys object versions; heavily churned keys (many re-uploads/deletes) under a versioned bucket.
Common situations: Time-travel or GC listings over buckets with heavy version churn; tooling that passes a conservative default max_keys; capacity probing that sets a small limit.
Related errors
- no items returned
- no 'updated' field
- Missing size (content length) header
- GCS PUT error \n\t {:?}
- Boundary string from 'multipart/related' HTTP upload occurre
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/21fac78ea8b22257.
Report an issue: GitHub.