Skip to content

Operations

Running Recached in production: what it exports, what to alert on, and which limits fail closed.

Metrics endpoint

Recached serves Prometheus metrics on a separate port from the cache itself, so you can expose it to your monitoring network without exposing the data plane.

bash
RECACHED_METRICS_PORT=9090 recached-server
curl http://127.0.0.1:9090/metrics

The port is set by RECACHED_METRICS_PORT and binds to the same host as RECACHED_BIND.

The metrics port has no authentication

It inherits RECACHED_BIND but not RECACHED_PASSWORD. Anything that can reach the port can read your metrics — including key hit/miss volume and per-command traffic. Bind it to a private interface or firewall it.

What is exported

Traffic metrics are event-driven:

MetricTypeLabelsMeaning
recached_commands_totalcountercommandCommands executed, by command name.
recached_command_errors_totalcountercommandCommands that returned an error, by command name.
recached_connections_totalcountertype = tcp | wsConnections accepted since start, split by transport.
recached_connections_activegaugeConnections currently open (TCP and WebSocket combined).
recached_keyspace_hits_totalcounterReads that found a live key.
recached_keyspace_misses_totalcounterReads that found nothing or an expired key.
recached_command_duration_secondshistogramcommandEnd-to-end store execution time by command.

Persistence and queue health

MetricTypeLabelsMeaning
recached_persistence_healthygauge1 while persistence is healthy; 0 after an AOF or checkpoint failure. Writes then return MISCONF until SAVE succeeds.
recached_persistence_errors_totalcounteroperationPersistence failures by operation; operation="all" is the aggregate.
recached_snapshot_saves_totalcounterstatusCompleted and failed checkpoints.
recached_snapshot_duration_secondshistogramTime a checkpoint holds the save and all-write barriers.
recached_last_successful_save_timestamp_secondsgaugeUnix timestamp of the last successful checkpoint.
recached_last_save_age_secondsgaugeSeconds since the last successful checkpoint.
recached_aof_bytesgaugeCurrent AOF file size when AOF is enabled.
recached_notification_overflows_totalcounterWATCH/QSUB clients disconnected because their bounded notification queue filled.
recached_sync_lag_disconnects_totalcounterWebSocket sync clients closed (code 4001) because they fell behind the mutation fan-out far enough to miss frames. They reconnect and resynchronise via qstate, so this is a backpressure signal, not data loss — but a sustained rate means clients cannot drain as fast as you are writing.
recached_pubsub_overflows_totalcounterPub/sub clients disconnected because their bounded delivery queue filled.

Capacity and sync

Sampled every 5 seconds, because these are levels rather than events.

MetricTypeMeaning
recached_memory_bytesgaugeIncremental logical bytes for stored keys and values. This is not process RSS. Compare it with RECACHED_MAX_MEMORY.
recached_keysgaugeMaintained stored-key count. Expired entries remain until bounded active expiry removes them. Compare it with RECACHED_MAX_KEYS.
recached_evictions_totalcounterKeys evicted since start. A rising rate means the cache is working at its cap.
recached_replicas_connectedgaugeReplicas currently attached to this primary.
recached_live_queriesgaugeRegistered QSUB patterns across all connections.
recached_watched_keysgaugeKeys under WATCH.
recached_dedup_clients_trackedgaugeClients with duplicate-suppression high-water marks in memory.
recached_replication_queue_depthgaugeDeepest replica send queue, in frames — work the primary has not yet put on the wire.
recached_replication_queue_bytesgaugeEncoded bytes in the deepest replica send queue.
recached_replication_lag_framesgaugeFrames the furthest-behind replica has been sent but has not acknowledged applying. Zero means every replica is caught up.
recached_replication_backlog_bytesgaugeBytes retained for partial replica resynchronization.
recached_replication_syncs_totalcounterFull and partial synchronizations, labeled by type.
recached_replication_sync_duration_secondshistogramInitial full or partial synchronization time, labeled by type.
recached_replication_disconnects_totalcounterReplica disconnects caused by the frame or byte queue limit, labeled by reason.

Recached does not implement SLOWLOG. The command histogram identifies which command class is slow; use client-side tracing when you need individual request attribution. Process RSS remains the capacity metric for allocator overhead, network buffers, and fragmentation.

Reading the two replication gauges

They fail differently, which is why both exist:

  • Queue depth high, lag high — the primary cannot hand frames off fast enough. The replica's channel is backing up, usually a slow or saturated network link. A replica whose queue fills is disconnected outright. It resumes from the retained backlog when possible and otherwise receives a fresh snapshot.
  • Queue depth zero, lag high — everything was written to the socket and the replica is not acknowledging it. The frames are in flight, or the replica is applying them slowly, or it is wedged. This is the case queue depth alone cannot see, and it is the one worth alerting on.

Lag is measured in frames, not bytes or seconds: one frame is one replicated write command. The RCP1 replication handshake rejects incompatible peers; upgrade primary and replicas together.

What is still not exported

  • Client outbox depth. That state lives in the browser — read it there with cache.pendingWrites().

Useful queries

promql
# Command throughput by command
rate(recached_commands_total[1m])

# Error ratio — the single most useful health signal
sum(rate(recached_command_errors_total[5m]))
  / sum(rate(recached_commands_total[5m]))

# Cache hit ratio
sum(rate(recached_keyspace_hits_total[5m]))
  / (sum(rate(recached_keyspace_hits_total[5m])) + sum(rate(recached_keyspace_misses_total[5m])))

# Connection headroom against RECACHED_MAX_CONNECTIONS (default 1024)
recached_connections_active

# WebSocket sync clients specifically
rate(recached_connections_total{type="ws"}[5m])

Suggested alerts

Thresholds are starting points — tune to your traffic.

AlertConditionWhy it matters
Error-rate spikeerror ratio > 1% for 5mUsually a client sending unsupported commands or malformed args after a deploy.
Connection saturationrecached_connections_active > 80% of RECACHED_MAX_CONNECTIONSNew connections are rejected once the semaphore is exhausted — this fails hard, not gracefully.
Hit ratio collapsehit ratio drops sharply vs baselineKeys expiring faster than expected, an eviction storm, or a cold restart.
Traffic flatlinerate(recached_commands_total[5m]) == 0 while clients are upThe process is alive enough to scrape but not serving.
Memory pressurerecached_memory_bytes > 80% of RECACHED_MAX_MEMORYEviction is about to start, or already has.
Eviction churnrate(recached_evictions_total[5m]) climbingThe working set no longer fits; results will start missing.
Replica lostrecached_replicas_connected dropsFailover risk — the standby is no longer following.
Replica falling behindrecached_replication_lag_frames > 1000 for 5mThe standby is not keeping up; a failover now would lose those writes.
Persistence unhealthyrecached_persistence_healthy == 0Writes are being refused after an AOF or checkpoint failure. Fix storage, then run SAVE.
Save stalledhigh recached_snapshot_duration_seconds or rising recached_last_save_age_secondsCheckpoints pause writers and may be blocked on storage.
Slow consumersincrease in either overflow counterA pub/sub, WATCH, or QSUB client cannot drain its bounded queue.

Health checking

There is no dedicated HTTP health endpoint. Use the protocol itself:

bash
# Liveness — is the cache answering?
redis-cli -p 6379 ping        # → PONG

# With auth enabled
redis-cli -p 6379 -a "$RECACHED_PASSWORD" ping

For container orchestration:

yaml
livenessProbe:
  exec:
    command: ["redis-cli", "-p", "6379", "ping"]
  initialDelaySeconds: 5
  periodSeconds: 10

The /metrics endpoint returning 200 proves the metrics listener is up, not that the cache is healthy — they are separate listeners. Probe the cache port for liveness and require recached_persistence_healthy == 1 for write readiness when persistence is enabled.

For a human-readable snapshot at a terminal — uptime, connected clients, keyspace size, replication role — use INFO:

bash
redis-cli -p 6379 INFO              # all default sections
redis-cli -p 6379 INFO replication  # just the topology

INFO and Prometheus serve different jobs and neither replaces the other: INFO is a point-in-time snapshot for an operator or a client's ready-check, while /metrics carries the per-command counters, error counts, and history that dashboards and alerts need. Alert on the metrics, not on scraped INFO output.

Capacity limits

Hard limits compiled into the server. Exceeding them produces errors rather than degradation, so it is worth knowing where the walls are:

LimitDefaultConfigurable
Max connections1024RECACHED_MAX_CONNECTIONS
Consecutive auth failures before disconnect5No
Read buffer per TCP connection64 MBNo
Queued commands per MULTI10,000RECACHED_MAX_MULTI_QUEUE
WATCHed keys per connection1,024RECACHED_MAX_WATCHES_PER_CONN
Live queries (QSUB) per connection64RECACHED_MAX_LIVE_QUERIES
Pub/sub channel and pattern subscriptions per connection1,024RECACHED_MAX_PUBSUB_SUBSCRIPTIONS
Keys allowed in a complete live-query initial state10,000RECACHED_MAX_QSUB_INITIAL_KEYS
Keys sampled per eviction pass10RECACHED_EVICTION_SAMPLE
Replication frame512 MBNo
WATCH/QSUB delivery queue256 messages and 8 MiB per connectionNo
Pub/sub delivery queue256 messages and 8 MiB per connectionNo
Replica delivery queue4,096 frames and 8 MiB per replicaRECACHED_REPL_BUFFER / RECACHED_REPL_BUFFER_BYTES
Partial-resync backlog16 MiB per primaryRECACHED_REPL_BACKLOG_BYTES
Glob pattern length (KEYS, SCAN MATCH, PSUBSCRIBE, sync scopes)1,024 bytesNo
Elements reserved up front for an aggregate1,024No
Client outbox (browser, offline writes)10,000 writesvia sync-client

The keyspace cap (RECACHED_MAX_KEYS) and memory cap (RECACHED_MAX_MEMORY) are configured rather than compiled — see Configuration.

Backup and restore

Snapshots are MessagePack files at RECACHED_SAVE_PATH, written atomically (temp file, fsync, rename, and directory fsync). A single snapshot read therefore sees a complete old or new file, but a durable deployment is a checkpoint set: snapshot, .dedup sidecar, and—when enabled—AOF. Do not copy those files one by one while writes or another save can run; that can combine files from different checkpoints.

bash
# Take a snapshot on demand, then confirm it completed
redis-cli -p 6379 BGSAVE
redis-cli -p 6379 LASTSAVE     # timestamp advances when the save lands

# Stop the server after SAVE, then copy the snapshot, its .dedup sidecar,
# and the AOF (when configured), or capture them with one atomic filesystem
# snapshot while the server is paused.

A sidecar file sits next to the snapshot with a .dedup extension, holding duplicate-suppression high-water marks. Back it up as part of the same checkpoint set: without it a restarted server can re-apply a write a client replays. A missing sidecar is a clean first boot; a present but corrupt sidecar is a startup error.

To restore, stop the server, put the snapshot (and its .dedup sidecar) at RECACHED_SAVE_PATH, and start it — both load at boot. There is no import path from a Redis RDB file; the formats are unrelated.

If AOF is enabled, the AOF replays on top of the snapshot. Losing the AOF while keeping the snapshot costs you every write since the last save.

Upgrades

Recached is pre-1.0 and the wire protocol is not frozen — see the protocol spec. Read the changelog before upgrading a minor version, and upgrade server and browser SDK together. The replication protocol identifies itself as RCP1; mixed replication protocol versions fail explicitly and are not supported.

The current versioned snapshot envelope accepts legacy bare-entry snapshots. The reverse is not guaranteed, so keep a complete copy of the pre-upgrade checkpoint set if you may need to roll back.

Released under the Apache License 2.0.