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.
📚 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 / Function | File | Purpose |
|---|---|---|
SystemPresence | system-presence.ts | Interface for presence entries: host, IP, version, platform, deviceFamily, lastInputSeconds, mode, deviceId, roles, scopes, instanceId, text, ts |
SystemPresenceUpdate | system-presence.ts | Tracks changes between presence states: { previous, next, changes } |
emitHeartbeatEvent(evt) | heartbeat-events.ts | Broadcasts a heartbeat event to all registered listeners via global singleton |
onHeartbeatEvent(listener) | heartbeat-events.ts | Subscribes to heartbeat events; returns an unsubscribe function |
getLastHeartbeatEvent() | heartbeat-events.ts | Returns the most recently emitted heartbeat event or undefined |
resetHeartbeatEventsForTest() | heartbeat-events.ts | Clears singleton state for deterministic test isolation |
pushSystemEvent(opts) | system-events.ts | Enqueues a session-scoped event with dedup and internal hook emission |
FixedWindowRateLimiter | fixed-window-rate-limit.ts | Rate limiter class with consume() and reset() methods |
createRateLimitRetryRunner(params) | retry-policy.ts | Factory for retry runners targeting rate-limited APIs |
createTelegramRetryRunner(params) | retry-policy.ts | Factory for retry runners tuned to Telegram API error patterns |
Data Structures
| Type | Name | Description |
|---|---|---|
| interface | SystemPresence | Presence record: host?, ip?, version?, platform?, deviceFamily?, lastInputSeconds?, mode?, deviceId?, roles?, scopes?, instanceId?, text, ts |
| interface | SystemPresenceUpdate | Delta object: { previous: SystemPresence, next: SystemPresence, changes: string[] } |
| interface | SystemEvent | Ephemeral event: text, ts, optional contextKey, deliveryContext, trusted |
| interface | RateLimitResult | Result of consume(): { allowed: boolean, retryAfterMs: number, remaining: number } |
| type | SessionQueue | Per-session bounded queue (max 20 entries) resolved via resolveGlobalMap<string, SessionQueue>() |
| symbol | Symbol.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
| Direction | Partner | Interface | Description |
|---|---|---|---|
| → | Service Layer | getPresence(deviceId) | Callers look up cached device presence instead of querying the network |
| → | API Gateway | FixedWindowRateLimiter.consume() | Gateway checks rate limit before forwarding outbound requests |
| → | WebSocket Handler | dedupe(payload) | Deduplicates incoming WebSocket messages before processing |
| ← | OS / Network | os.hostname(), networkInterfaces() | Self-presence init reads hostname, IP, macOS model, and version |
| ↔ | Internal Hooks | pushSystemEvent(opts) | System events emit to internal hooks for cross-module coordination |
| → | External APIs | createRateLimitRetryRunner() | Retry runners wrap HTTP calls with exponential backoff on 429 / transient errors |
Configuration
| Knob / Parameter | Default | Description |
|---|---|---|
presenceTTL | 300 000 ms (5 min) | Time-to-live for each entry in the system-presence map before eviction |
maxPresenceEntries | 200 | Hard cap on number of presence entries in the global map |
maxQueueSize | 20 | Maximum number of events per session queue in system-events |
retryAttempts | 3 | Number of retry attempts for rate-limit and Telegram retry runners |
retryMinDelay | 400 ms | Minimum delay between retries (before jitter) |
retryMaxDelay | 30 000 ms (30 s) | Maximum delay cap for exponential backoff |
retryJitter | 0.1 | Jitter factor applied to retry delays to prevent thundering herd |
retryPattern | /429|timeout|connect|reset|closed|unavailable|temporarily/i | Regex 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.
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.
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.
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}`));