Context
S3Bucket._url_cache (added in #661 ) exists so that re-presigning the same object returns the same URL string for a while. boto3 stamps the signing clock into every signature, so without it each poll produced a different URL and the browser re-downloaded frames it already had. The cache key is (bucket_key, url_expiration, window_slot) — see src/app/services/storage.py:155.
Problem
The window slot is part of the key, so when the window rolls over (1h at the default S3_URL_EXPIRATION=24h), every entry from the previous slot becomes permanently unreachable — a lookup can never match an old slot again. Nothing removes them. They only leave through the size-based LRU eviction at src/app/services/storage.py:161-165.
Consequence: an active organization climbs to the 8192-entry ceiling and stays there permanently, with a substantial share of it dead weight from expired windows. Effective capacity is roughly half the nominal figure.
Why this is safe to fix
Worth distinguishing from the existing comment at src/app/services/storage.py:162-164, which argues against clearing the cache. That argument is correct for a full clear() — it would re-presign every URL currently in flight, changing them exactly when stability matters most.
It does not apply to purging stale slots. Those entries are already unreachable by construction, so removing them changes nothing observable: no URL being handed out is affected, and no client sees a different string.
Proposal
A two-generation cache: keep a current dict and a previous dict, swap on slot rollover and drop the old one. O(1), no scan, no per-entry timestamp bookkeeping. The window_slot component of the key becomes redundant and can go.
Impact
Roughly doubles usable capacity for the same memory. No behavioral change for clients. Non-urgent — nothing breaks today, the cache is bounded and a miss only costs a local HMAC signature (generate_presigned_url makes no S3 call).
Related: #671 , which covers the size of the bound itself.
Happy to discuss it :)
Co authored with Claude
Context
S3Bucket._url_cache(added in #661 ) exists so that re-presigning the same object returns the same URL string for a while. boto3 stamps the signing clock into every signature, so without it each poll produced a different URL and the browser re-downloaded frames it already had. The cache key is(bucket_key, url_expiration, window_slot)— seesrc/app/services/storage.py:155.Problem
The window slot is part of the key, so when the window rolls over (1h at the default
S3_URL_EXPIRATION=24h), every entry from the previous slot becomes permanently unreachable — a lookup can never match an old slot again. Nothing removes them. They only leave through the size-based LRU eviction atsrc/app/services/storage.py:161-165.Consequence: an active organization climbs to the 8192-entry ceiling and stays there permanently, with a substantial share of it dead weight from expired windows. Effective capacity is roughly half the nominal figure.
Why this is safe to fix
Worth distinguishing from the existing comment at
src/app/services/storage.py:162-164, which argues against clearing the cache. That argument is correct for a fullclear()— it would re-presign every URL currently in flight, changing them exactly when stability matters most.It does not apply to purging stale slots. Those entries are already unreachable by construction, so removing them changes nothing observable: no URL being handed out is affected, and no client sees a different string.
Proposal
A two-generation cache: keep a current dict and a previous dict, swap on slot rollover and drop the old one. O(1), no scan, no per-entry timestamp bookkeeping. The
window_slotcomponent of the key becomes redundant and can go.Impact
Roughly doubles usable capacity for the same memory. No behavioral change for clients. Non-urgent — nothing breaks today, the cache is bounded and a miss only costs a local HMAC signature (
generate_presigned_urlmakes no S3 call).Related: #671 , which covers the size of the bound itself.
Happy to discuss it :)
Co authored with Claude