Skip to content

Purge stale-window entries from the presigned-URL cache #672

Description

@fe51

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions