Infrastructure/caching/ — In-Memory State & Rate Limiting

In-memory caching, heartbeat coordination, and fixed-window rate limiting for OpenClaw services

The caching sub-module manages in-memory state maps, session-scoped event queues, heartbeat event coordination, and rate limiting. It prevents redundant work, deduplicates payloads, and protects external APIs from being overwhelmed by applying fixed-window rate limits with automatic retry-after computation.

6+
Cache Patterns
~15
Files
~4K
Lines
2
Rate Limiter Types

📚 What is this & why does it exist?

The caching layer prevents redundant work and protects external APIs from being overwhelmed. Instead of hitting disk or network for every status check, the module maintains in-memory maps keyed by device ID or session key. Rate limiters use fixed-window algorithms that track consumption counts per time window, automatically computing retry-after delays. Heartbeat and system event buses coordinate ephemeral state across the application without persistent storage.

💡 Real-world analogy

Imagine a busy restaurant kitchen with a whiteboard (in-memory cache) near the door. Instead of walking to the storeroom every time to check ingredient stock, the chef glances at the whiteboard. The board has a timer—every 5 minutes the stock clerk wipes it clean and writes fresh counts. If too many orders come in at once, a bouncer (rate limiter) at the door tells new customers “please wait 30 seconds.” Meanwhile, a bell system (heartbeat events) lets the front-of-house know when the kitchen is alive and processing, while a ticket rail (system events) holds at most 20 active order slips per station to prevent overload.

Contents

Architecture

The caching sub-module is organized around three pillars: presence & state maps (system-presence), event buses (heartbeat-events, system-events), and rate control (fixed-window-rate-limit, retry-policy, dedupe). External callers interact through clean functional interfaces while internal state is managed via global singletons and scoped maps.

flowchart TD
  subgraph Callers
    SVC["Service Layer"]
    API["API Gateway"]
    WS["WebSocket Handler"]
  end

  subgraph caching["caching/"]
    SP["system-presence.ts\nIn-memory map\n5-min TTL, max 200"]
    HB["heartbeat-events.ts\nGlobal singleton bus\nSymbol.for keyed"]
    SE["system-events.ts\nSession-scoped queues\nMax 20 per queue"]
    RL["fixed-window-rate-limit.ts\nFixedWindowRateLimiter\nconsume / reset"]
    RP["retry-policy.ts\nExponential backoff\n3 attempts, jitter"]
    DD["dedupe.ts\nPayload deduplication"]
  end

  SVC --> SP
  SVC --> HB
  SVC --> SE
  API --> RL
  API --> RP
  WS --> DD
  RL --> RP
  SE --> DD
  SP --> HB

Key Classes & Functions

Class / FunctionFilePurpose
SystemPresencesystem-presence.tsInterface for presence entries: host, IP, version, platform, deviceFamily, lastInputSeconds, mode, deviceId, roles, scopes, instanceId, text, ts
SystemPresenceUpdatesystem-presence.tsTracks changes between presence states: { previous, next, changes }
emitHeartbeatEvent(evt)heartbeat-events.tsBroadcasts a heartbeat event to all registered listeners via global singleton
onHeartbeatEvent(listener)heartbeat-events.tsSubscribes to heartbeat events; returns an unsubscribe function
getLastHeartbeatEvent()heartbeat-events.tsReturns the most recently emitted heartbeat event or undefined
resetHeartbeatEventsForTest()heartbeat-events.tsClears singleton state for deterministic test isolation
pushSystemEvent(opts)system-events.tsEnqueues a session-scoped event with dedup and internal hook emission
FixedWindowRateLimiterfixed-window-rate-limit.tsRate limiter class with consume() and reset() methods
createRateLimitRetryRunner(params)retry-policy.tsFactory for retry runners targeting rate-limited APIs
createTelegramRetryRunner(params)retry-policy.tsFactory for retry runners tuned to Telegram API error patterns

Data Structures

TypeNameDescription
interfaceSystemPresencePresence record: host?, ip?, version?, platform?, deviceFamily?, lastInputSeconds?, mode?, deviceId?, roles?, scopes?, instanceId?, text, ts
interfaceSystemPresenceUpdateDelta object: { previous: SystemPresence, next: SystemPresence, changes: string[] }
interfaceSystemEventEphemeral event: text, ts, optional contextKey, deliveryContext, trusted
interfaceRateLimitResultResult of consume(): { allowed: boolean, retryAfterMs: number, remaining: number }
typeSessionQueuePer-session bounded queue (max 20 entries) resolved via resolveGlobalMap<string, SessionQueue>()
symbolSymbol.for("openclaw.heartbeatEvents.state")Global key for the heartbeat singleton, ensuring cross-module identity

Interaction Flow

This sequence diagram shows a typical flow when a service checks presence, rate-limits an outbound API call, and falls back through the retry policy on a 429 response.

sequenceDiagram
  participant S as Service Layer
  participant SP as system-presence
  participant RL as FixedWindowRateLimiter
  participant RP as retry-policy
  participant API as External API

  S->>SP: getPresence(deviceId)
  SP-->>S: SystemPresence or undefined

  S->>RL: consume()
  alt allowed
    RL-->>S: { allowed: true, remaining: N }
    S->>API: POST /send
    API-->>S: 200 OK
  else rate limited
    RL-->>S: { allowed: false, retryAfterMs: 1200 }
    S->>RP: createRateLimitRetryRunner()
    RP->>API: POST /send (attempt 1)
    API-->>RP: 429 Too Many Requests
    Note over RP: wait 400ms + jitter
    RP->>API: POST /send (attempt 2)
    API-->>RP: 200 OK
    RP-->>S: result
  end

Interactions

DirectionPartnerInterfaceDescription
→Service LayergetPresence(deviceId)Callers look up cached device presence instead of querying the network
→API GatewayFixedWindowRateLimiter.consume()Gateway checks rate limit before forwarding outbound requests
→WebSocket Handlerdedupe(payload)Deduplicates incoming WebSocket messages before processing
←OS / Networkos.hostname(), networkInterfaces()Self-presence init reads hostname, IP, macOS model, and version
↔Internal HookspushSystemEvent(opts)System events emit to internal hooks for cross-module coordination
→External APIscreateRateLimitRetryRunner()Retry runners wrap HTTP calls with exponential backoff on 429 / transient errors

Configuration

Knob / ParameterDefaultDescription
presenceTTL300 000 ms (5 min)Time-to-live for each entry in the system-presence map before eviction
maxPresenceEntries200Hard cap on number of presence entries in the global map
maxQueueSize20Maximum number of events per session queue in system-events
retryAttempts3Number of retry attempts for rate-limit and Telegram retry runners
retryMinDelay400 msMinimum delay between retries (before jitter)
retryMaxDelay30 000 ms (30 s)Maximum delay cap for exponential backoff
retryJitter0.1Jitter factor applied to retry delays to prevent thundering herd
retryPattern/429|timeout|connect|reset|closed|unavailable|temporarily/iRegex pattern matching transient error messages that trigger retry

Directory Map

Source tree for caching/
caching/
├── system-presence.ts           ← Global in-memory presence map (5-min TTL, max 200)
├── heartbeat-events.ts          ← Singleton heartbeat event bus via Symbol.for
├── system-events.ts             ← Session-scoped ephemeral event queues (max 20)
├── fixed-window-rate-limit.ts   ← FixedWindowRateLimiter: consume() / reset()
├── retry-policy.ts              ← Exponential backoff runners (rate-limit & Telegram)
├── dedupe.ts                    ← Payload deduplication utilities
├── cache_strategies/            ← Deep-dive: TTL, LRU, and eviction strategies
├── invalidation/                ← Deep-dive: Cache invalidation patterns and hooks
└── memory_management/           ← Deep-dive: Bounded maps and memory pressure handling

Deep-Dive Highlights

Core mechanisms from the caching sub-module have been extracted into dedicated focus pages for in-depth exploration. Each page covers one algorithm, protocol, or subsystem with diagrams, code walks, and configuration details.

Deep-Dive Pages

Cache Strategies Focus

TTL-based eviction, LRU ordering, and bounded-map strategies used across system-presence and session queues. Covers how entries expire after 5 minutes and how the 200-entry cap is enforced.

Extracted topic page

Cache Invalidation Focus

How stale entries are detected and purged: presence TTL sweeps, event queue dedup checks, and the resetHeartbeatEventsForTest() pattern for test isolation. Includes hook-driven invalidation flows.

Extracted topic page

Memory Management Focus

Bounded data structures that prevent unbounded growth: max 200 presence entries, max 20 events per session queue, and global singleton patterns via Symbol.for to avoid duplicate allocations.

Extracted topic page

Code Examples

Example: Checking and Updating System Presence

import { getPresence, updatePresence } from "./system-presence";
import type { SystemPresence, SystemPresenceUpdate } from "./system-presence";

// Look up cached presence for a device
const current: SystemPresence | undefined = getPresence("device-abc-123");

if (!current) {
  // Self-presence initialization: hostname, IP, macOS model, version
  const entry: SystemPresence = {
    host: os.hostname(),
    ip: getLocalIP(),
    version: "2.4.1",
    platform: process.platform,
    deviceFamily: getMacModel(),
    lastInputSeconds: 0,
    mode: "active",
    deviceId: "device-abc-123",
    roles: ["editor"],
    scopes: ["read", "write"],
    instanceId: randomUUID(),
    text: "Online",
    ts: Date.now(),
  };

  // Stored with 5-min TTL, auto-evicted when map exceeds 200 entries
  const update: SystemPresenceUpdate = updatePresence("device-abc-123", entry);
  console.log("Changed fields:", update.changes);
}

Example: Subscribing to Heartbeat Events

import {
  emitHeartbeatEvent,
  onHeartbeatEvent,
  getLastHeartbeatEvent,
} from "./heartbeat-events";

// Subscribe — returns an unsubscribe function
const unsubscribe = onHeartbeatEvent((evt) => {
  console.log("Heartbeat received:", evt);
});

// Emit a heartbeat (broadcasts to all listeners via global singleton)
emitHeartbeatEvent({ ts: Date.now(), status: "alive" });

// Query the most recent event without subscribing
const last = getLastHeartbeatEvent();
console.log("Last heartbeat:", last?.ts);

// Cleanup
unsubscribe();

Example: Fixed-Window Rate Limiting with Retry

import { FixedWindowRateLimiter } from "./fixed-window-rate-limit";
import { createRateLimitRetryRunner } from "./retry-policy";

// Allow 100 requests per 60-second window
const limiter = new FixedWindowRateLimiter({
  maxRequests: 100,
  windowMs: 60_000,
});

const result = limiter.consume();
// result: { allowed: boolean, retryAfterMs: number, remaining: number }

if (result.allowed) {
  await fetch("https://api.example.com/data");
} else {
  console.warn(`Rate limited. Retry after ${result.retryAfterMs}ms`);
}

// Retry runner: 3 attempts, 400ms min, 30s max, 0.1 jitter
// Matches: /429|timeout|connect|reset|closed|unavailable|temporarily/i
const runner = createRateLimitRetryRunner({
  attempts: 3,
  minDelay: 400,
  maxDelay: 30_000,
  jitter: 0.1,
});

const response = await runner(() => fetch("https://api.example.com/send"));

Example: Pushing Session-Scoped System Events

import { pushSystemEvent, getSessionEvents } from "./system-events";
import type { SystemEvent } from "./system-events";

// Push an event into the session-scoped queue (max 20 per queue)
// Automatically deduplicates and emits to internal hooks
pushSystemEvent({
  text: "User connected",
  contextKey: "session-xyz",
  deliveryContext: "websocket",
  trusted: true,
});

// Retrieve events for a session
const events: SystemEvent[] = getSessionEvents("session-xyz");
events.forEach((e) => console.log(`[${e.ts}] ${e.text}`));