You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
`sandbox:storage:global:expiration` is a single global ZSET shared by every
sandbox across all teams. The evictor calls `ExpiredItems` every 50 ms
(`pollInterval`), which issues a `ZRANGEBYSCORE` against this key on every tick.
At `pollInterval = 50 ms` and N API allocations, the evictor issues 20 × N `ZRANGEBYSCORE` calls per second against the 22 k-member ZSET.
This load is currently invisible — there is no metric showing either the
key size trend or the per-sweep cost.
Observability gap
Signal
Metric name (proposed)
Collection point
ZSET cardinality
`api.redis_storage.expiration_index.size` (gauge)
Once per heal pass (`healExpirationIndex`, every 5 min)
Why a gauge for size, not sampled on every evictor tick?
`global:expiration` is already the key being diagnosed as a high-frequency
hot key. Sampling `ZCARD` on every 50 ms evictor tick would add 20 × N
extra Redis calls per second — worsening the problem while observing it.
A single `ZCARD` piggybacked on the existing heal pass (every 5 min,
same cadence as #3604) adds negligible overhead and is sufficient for
capacity planning and alerting.
Why sweep duration matters
`ZRANGEBYSCORE` is O(log N + K) where N = 22,458 and K = batch size (256).
As N grows the per-sweep cost grows. Correlating sweep duration with ZSET
size over time makes the relationship visible before it becomes a latency
incident.
Background
`sandbox:storage:global:expiration` is a single global ZSET shared by every
sandbox across all teams. The evictor calls `ExpiredItems` every 50 ms
(`pollInterval`), which issues a `ZRANGEBYSCORE` against this key on every tick.
Problem
There are currently no metrics to observe:
`sandbox:storage:global:expiration` is growing unboundedly, shrinking after
orphan sweeps (fix(api): prune stale team index entries when ZSET orphans are swept #3567), or stable at steady state.
`ZRANGEBYSCORE + MGET pipeline` takes, or whether sweep latency is
increasing as the ZSET grows.
Without these two signals it is impossible to:
reducing cardinality
Production evidence
Observed 2026-08-14 09:00 – 09:30 (Redis big-key scan):
At `pollInterval = 50 ms` and N API allocations, the evictor issues
20 × N `ZRANGEBYSCORE` calls per second against the 22 k-member ZSET.
This load is currently invisible — there is no metric showing either the
key size trend or the per-sweep cost.
Observability gap
Why a gauge for size, not sampled on every evictor tick?
`global:expiration` is already the key being diagnosed as a high-frequency
hot key. Sampling `ZCARD` on every 50 ms evictor tick would add 20 × N
extra Redis calls per second — worsening the problem while observing it.
A single `ZCARD` piggybacked on the existing heal pass (every 5 min,
same cadence as #3604) adds negligible overhead and is sufficient for
capacity planning and alerting.
Why sweep duration matters
`ZRANGEBYSCORE` is O(log N + K) where N = 22,458 and K = batch size (256).
As N grows the per-sweep cost grows. Correlating sweep duration with ZSET
size over time makes the relationship visible before it becomes a latency
incident.
Related
`global:expiration` via orphaned ZSET members
`startTransitionScript` (separate hot path)
sweep latency metrics would provide the baseline needed to evaluate
whether a backend switch is warranted
/cc @jakubno @dobrac @ValentaTomas