getHealthCache() returns HealthSummary | nullGateway Health Monitoring System
The health check subsystem continuously monitors the Gateway's operational status. It exposes cached health summaries via getHealthCache(), tracks versions with getHealthVersion(), and provides structured error logging through the logHealth interface — all integrated with the GatewayRequestContext and scheduled by CronService.
getHealthCache() returns HealthSummary | nullgetHealthVersion() returns monotonic versionlogHealth: {error: (msg: string) => void}CronService runs checks every 30 secondsHealth checks serve two primary purposes: providing a readiness signal for load balancers and orchestrators (Kubernetes, systemd), and offering internal diagnostics for operators monitoring the Gateway's subsystems. The health system runs as a CronService task that periodically evaluates all registered health probes and caches the aggregated result.
External consumers (HTTP health endpoints, monitoring dashboards) call getHealthCache() to retrieve the latest cached HealthSummary without triggering a fresh evaluation. The getHealthVersion() function returns a monotonically increasing counter that increments each time the health status changes, allowing efficient polling via ETags or long-polling.
CronService and never block incoming requests. The cached result ensures that even under heavy load, health endpoints respond in constant time.
The following flowchart illustrates the complete health check cycle, from scheduled trigger through cache update and version increment.
flowchart TD
A[CronService Trigger] --> B[Collect Health Probes]
B --> C{All probes registered?}
C -- No --> D[Log warning: missing probes]
D --> E[Run available probes]
C -- Yes --> E
E --> F[Evaluate each probe]
F --> G{Any probe failed?}
G -- Yes --> H[Aggregate: status = unhealthy]
G -- No --> I{Any probe degraded?}
I -- Yes --> J[Aggregate: status = degraded]
I -- No --> K[Aggregate: status = healthy]
H --> L[Build HealthSummary]
J --> L
K --> L
L --> M[Compare with cached summary]
M --> N{Status changed?}
N -- Yes --> O[Increment healthVersion]
O --> P[Update healthCache]
N -- No --> P
P --> Q[Log health result]
Q --> R[Schedule next check]
The HealthSummary is the primary data structure returned by getHealthCache(). It contains the overall status, individual probe results, timestamps, and diagnostic metadata used by monitoring systems.
type HealthStatus = "healthy" | "degraded" | "unhealthy";
interface HealthSummary {
status: HealthStatus;
version: number;
checkedAtMs: number;
durationMs: number;
probes: HealthProbeResult[];
metadata: {
gatewayId: string;
uptime: number;
activeConnections: number;
pendingRequests: number;
};
}
interface HealthProbeResult {
name: string;
status: HealthStatus;
message: string;
durationMs: number;
lastSuccessMs: number | null;
consecutiveFailures: number;
}
// The cache accessor — returns the latest cached summary or null if
// no health check has run yet (e.g., during initial boot)
function getHealthCache(): HealthSummary | null {
return healthCacheStore.get();
}
// Returns a monotonically increasing version counter that increments
// each time the health status changes (not on every check cycle)
function getHealthVersion(): number {
return healthCacheStore.getVersion();
}
getHealthCache() returns null until the first health check cycle completes (typically within 30 seconds of boot). Load balancers should treat null as "not yet ready" and avoid routing traffic until a non-null summary with status "healthy" is available.
During boot, each subsystem registers a health probe with the HealthCheckManager. A probe is a named async function that evaluates one aspect of the Gateway's health. Probes are registered in the GatewayRequestContext initialization phase.
interface HealthProbe {
name: string;
check: () => Promise<HealthProbeResult>;
timeoutMs: number;
critical: boolean;
}
class HealthCheckManager {
private probes: Map<string, HealthProbe> = new Map();
private cache: HealthSummary | null = null;
private version: number = 0;
registerProbe(probe: HealthProbe): void {
if (this.probes.has(probe.name)) {
logHealth.error(`Duplicate probe registration: ${probe.name}`);
return;
}
this.probes.set(probe.name, probe);
}
async runAllProbes(ctx: GatewayRequestContext): Promise<HealthSummary> {
const start = Date.now();
const results: HealthProbeResult[] = [];
for (const [name, probe] of this.probes) {
try {
const result = await Promise.race([
probe.check(),
timeout(probe.timeoutMs, name),
]);
results.push(result);
} catch (err) {
results.push({
name,
status: "unhealthy",
message: `Probe threw: ${String(err)}`,
durationMs: Date.now() - start,
lastSuccessMs: null,
consecutiveFailures: 1,
});
}
}
const overallStatus = this.aggregateStatus(results);
const summary: HealthSummary = {
status: overallStatus,
version: this.version,
checkedAtMs: Date.now(),
durationMs: Date.now() - start,
probes: results,
metadata: {
gatewayId: ctx.gatewayId,
uptime: Date.now() - ctx.startedAtMs,
activeConnections: ctx.nodeRegistry.size(),
pendingRequests: ctx.metrics.getPendingCount(),
},
};
// Only increment version on status change
if (!this.cache || this.cache.status !== summary.status) {
this.version++;
summary.version = this.version;
}
this.cache = summary;
return summary;
}
private aggregateStatus(results: HealthProbeResult[]): HealthStatus {
if (results.some(r => r.status === "unhealthy")) return "unhealthy";
if (results.some(r => r.status === "degraded")) return "degraded";
return "healthy";
}
}
The health check cycle is registered as a CronService task during boot. It runs every 30 seconds and updates the health cache after each cycle.
// Registered during initializeServices() in boot sequence
cronService.registerTask(
"health-check",
"*/30 * * * * *",
async () => {
const summary = await healthCheckManager.runAllProbes(ctx);
if (summary.status === "unhealthy") {
logHealth.error(
`Health check FAILED: ${summary.probes
.filter(p => p.status === "unhealthy")
.map(p => p.name)
.join(", ")}`
);
}
ctx.logger.debug(
`Health check: ${summary.status} (v${summary.version}, ${summary.durationMs}ms)`
);
}
);
The Gateway exposes a /health HTTP endpoint that returns the cached health summary. This endpoint is used by load balancers for readiness probes and by monitoring dashboards for status display.
httpRouter.get("/health", (req, res) => {
const cached = getHealthCache();
if (!cached) {
res.status(503).json({ status: "not-ready", message: "Health check pending" });
return;
}
const statusCode = cached.status === "healthy" ? 200
: cached.status === "degraded" ? 200
: 503;
res.setHeader("ETag", `"v${cached.version}"`);
res.setHeader("Cache-Control", "no-cache, must-revalidate");
res.status(statusCode).json(cached);
});
httpRouter.get("/health/version", (req, res) => {
res.json({ version: getHealthVersion() });
});
The logHealth object provides a dedicated error logging interface for health-related issues. It writes to a separate log stream so health diagnostics don't get buried in request logs.
const logHealth: { error: (message: string) => void } = {
error: (message: string) => {
const entry = {
level: "error",
category: "health",
timestamp: new Date().toISOString(),
message,
};
process.stderr.write(JSON.stringify(entry) + "\n");
// Also increment the health error counter for metrics
ctx.metrics.increment("health.errors");
},
};
This sequence diagram shows a complete health check cycle including probe execution, cache update, and external consumer access.
sequenceDiagram
participant CS as CronService
participant HM as HealthCheckManager
participant WS as WebSocket Probe
participant DB as Session Store Probe
participant NR as Node Registry Probe
participant HC as Health Cache
participant LB as Load Balancer
CS->>HM: runAllProbes(ctx)
par Probe execution
HM->>WS: check()
WS-->>HM: {status: "healthy", durationMs: 2}
and
HM->>DB: check()
DB-->>HM: {status: "healthy", durationMs: 15}
and
HM->>NR: check()
NR-->>HM: {status: "degraded", message: "3 stale nodes"}
end
HM->>HM: aggregateStatus() → "degraded"
HM->>HM: Compare with previous status
alt status changed
HM->>HM: Increment version
end
HM->>HC: Update cache with new HealthSummary
Note over LB: Periodic readiness probe
LB->>HC: GET /health
HC-->>LB: 200 {status: "degraded", version: 42}
LB->>LB: Mark Gateway as degraded
The Gateway ships with several built-in health probes that are automatically registered during boot. Custom probes can be added via the registerProbe() API.
| Probe Name | What It Checks | Timeout | Critical |
|---|---|---|---|
websocket-server | WebSocket server is accepting connections | 5s | Yes |
session-store | Session store is readable and writable | 10s | Yes |
node-registry | NodeRegistry has no stale entries exceeding threshold | 5s | No |
cron-service | CronService event loop is responsive | 3s | Yes |
memory-usage | Heap usage below 90% of max | 1s | No |
disk-space | Snapshot directory has sufficient free space | 5s | No |
"unhealthy". Non-critical probes set it to "degraded". This distinction allows load balancers to differentiate between "must not receive traffic" and "can receive traffic but performance may be impacted."
The health cache uses a write-on-schedule pattern — it is only updated when the CronService triggers a new check cycle. Between cycles, the cache returns the same HealthSummary instance. The version field allows efficient change detection.
| Event | Cache Behavior | Version Impact |
|---|---|---|
| Scheduled check completes | Cache updated with new summary | Incremented only if status changed |
| Probe timeout | Probe marked "unhealthy" in summary | Incremented if overall status changed |
| Gateway shutdown initiated | Cache set to "unhealthy" immediately | Incremented unconditionally |
| Manual invalidation | Cache cleared; next getHealthCache() returns null | Reset to 0 |
| Between check cycles | Cache returns stale (up to 30s old) summary | Unchanged |
The health system is wired into the GatewayRequestContext during boot. The context holds references to the HealthCheckManager, the cache accessor functions, and the logHealth logger. This integration ensures that all subsystems can contribute health probes and query health status without importing global singletons.
// Fields added to GatewayRequestContext for health integration
interface GatewayRequestContext {
// ... other fields ...
healthCache: {
get: () => HealthSummary | null;
getVersion: () => number;
invalidate: () => void;
};
healthCheckManager: HealthCheckManager;
logHealth: {
error: (message: string) => void;
};
}
// During buildGatewayRequestContext():
const healthCheckManager = new HealthCheckManager();
const ctx: GatewayRequestContext = {
// ...
healthCache: {
get: () => healthCheckManager.getCache(),
getVersion: () => healthCheckManager.getVersion(),
invalidate: () => healthCheckManager.clearCache(),
},
healthCheckManager,
logHealth,
};
| Scenario | Behavior | Operator Impact |
|---|---|---|
| Probe throws exception | Caught; probe marked "unhealthy" with error message | Check logs for health.errors metric |
| Probe times out | Probe marked "unhealthy" with "timeout" message | Investigate slow subsystem |
| All probes healthy | Status "healthy", version unchanged | No action needed |
| CronService itself unhealthy | Health checks stop running; cache becomes stale | Monitor checkedAtMs age in cache |
| Memory pressure | memory-usage probe returns "degraded" | Consider scaling or restarting |
| Graceful shutdown | Cache immediately set to "unhealthy" | LB drains connections |
getHealthCache().checkedAtMs is more than 90 seconds old, it likely means the CronService has stopped running. Monitoring systems should alert when Date.now() - checkedAtMs > 90000.
| Parameter | Type | Default | Description |
|---|---|---|---|
health.checkIntervalSec | number | 30 | Seconds between health check cycles |
health.probeTimeoutMs | number | 10000 | Default timeout per probe |
health.staleThresholdMs | number | 90000 | Age after which cache is considered stale |
health.memoryThreshold | number | 0.9 | Heap usage fraction triggering degraded |
health.diskMinFreeMb | number | 100 | Minimum free disk space in MB |
src/gateway/
├── health.ts ← HealthCheckManager, getHealthCache(), getHealthVersion()
├── health-probes.ts ← Built-in probe implementations
├── health-types.ts ← HealthSummary, HealthProbeResult, HealthStatus
├── health-log.ts ← logHealth interface and error stream
├── cron.ts ← CronService, health check task scheduling
└── context.ts ← GatewayRequestContext health integration