Skip to content

obs(api): no metrics on global expiration ZSET size or evictor sweep latency #3605

Description

@AdaAibaby

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:

  1. ZSET cardinality over time — no way to know whether
    `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.
  2. Evictor sweep duration — no way to know how long each
    `ZRANGEBYSCORE + MGET pipeline` takes, or whether sweep latency is
    increasing as the ZSET grows.

Without these two signals it is impossible to:

Production evidence

Observed 2026-08-14 09:00 – 09:30 (Redis big-key scan):

Key Type Memory Elements
`sandbox:storage:global:expiration` zset 3,137,528 B (~3 MB) 22,458
`sandbox:storage:{65380065...}:index` set 588,824 B 9,535
`template:info:{fnqz33l5lhnpqiephgbl}:default` string 12,304 B 1

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)
Evictor sweep duration `api.redis_storage.expiration_index.sweep_duration` (histogram, ms) Per `ExpiredItems` call

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

/cc @jakubno @dobrac @ValentaTomas

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