{"record":{"id":"21fac78ea8b22257","repo":"neondatabase/neon","slug":"max-keys-reached","errorCode":null,"errorMessage":"max keys reached","messagePattern":"max keys reached","errorType":"exception","errorClass":"DownloadError","httpStatus":null,"severity":"warning","filePath":"libs/remote_storage/src/gcs_bucket.rs","lineNumber":304,"sourceCode":"                               )\n                           )\n                        }\n                        Ok(\n                            GCSVersion {\n                                key: self.gcs_object_to_relative_path(&name),\n                                last_modified: to_system_time(updated).unwrap(),\n                                id: VersionId(generation.expect(\"no version id\")),\n                                time_deleted: to_system_time(time_deleted),\n                            }\n                        )\n                    }).collect::<Result<Vec<GCSVersion>, _>>();\n                \n            versions.versions.extend(version_listing?);\n\n            if let Some(max_keys) = max_keys {\n                if versions.versions.len() >= max_keys.get().try_into().unwrap() {\n                    return Err(DownloadError::Other(\n                        anyhow::anyhow!(\"max keys reached\") \n                    ));\n                }\n            }\n            \n            if continuation_token.is_none() {\n                break\n            }\n        }\n        \n        Ok(versions)\n        \n    }\n\n    async fn put_object(\n        &self,\n        byte_stream: impl Stream<Item = std::io::Result<Bytes>> + Send + Sync + 'static,\n        fs_size: usize,\n        to: &RemotePath,","sourceCodeStart":286,"sourceCodeEnd":322,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/remote_storage/src/gcs_bucket.rs#L286-L322","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: hard error once the limit is hit\nif let Some(max_keys) = max_keys {\n    if versions.versions.len() >= max_keys.get().try_into().unwrap() {\n        return Err(DownloadError::Other(anyhow::anyhow!(\"max keys reached\")));\n    }\n}\n\n// after: truncate explicitly and signal, rather than erroring\nif let Some(max_keys) = max_keys {\n    let limit: usize = max_keys.get().try_into().unwrap();\n    if versions.versions.len() >= limit {\n        versions.versions.truncate(limit);\n        versions.truncated = true;\n        return Ok(versions);\n    }\n}","handlingStrategy":"validation","validationCode":"// Size max_keys to the expected version count before calling (e.g. from prior metrics).\nconst EXPECTED_VERSIONS_PER_KEY: usize = 1000;\nlet max_keys = MaxKeys::new(EXPECTED_VERSIONS_PER_KEY.next_power_of_two());\n// or omit max_keys entirely when the caller can stream the full set","typeGuard":null,"tryCatchPattern":"// Treat 'max keys reached' as a truncation signal, not a crash.\nlet listing = match storage.list_versions(key, Some(max_keys), &cancel).await {\n    Ok(v) => v,\n    Err(DownloadError::Other(e)) if format!(\"{e:#}\").contains(\"max keys reached\") => {\n        tracing::warn!(\"listing for {key} hit max_keys={}; continuing with degraded history\", max_keys.get());\n        // escalate to a paginated walk without the limit, or degrade gracefully\n        return handle_truncated(key).await;\n    }\n    Err(e) => return Err(e),\n};","preventionTips":["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"],"tags":["gcs","google-cloud-storage","listing","pagination","limits"],"backgroundTag":"pagination-limit-exceeded","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}