# Circuit Breaker and Fallback Patterns in WorldMonitor: Handling External API Failures Gracefully

> Discover how WorldMonitor uses circuit breaker and fallback patterns to gracefully handle external API failures with caching, persistence, and default values, ensuring dashboard responsiveness.

- Repository: [Elie Habib/worldmonitor](https://github.com/koala73/worldmonitor)
- Tags: best-practices
- Published: 2026-03-09

---

**WorldMonitor uses a centralized circuit-breaker utility in [`src/utils/circuit-breaker.ts`](https://github.com/koala73/worldmonitor/blob/main/src/utils/circuit-breaker.ts) that combines failure detection, cooldown periods, and multiple fallback layers—including in-memory caching, stale-while-revalidate, IndexedDB persistence, and default value returns—to ensure the dashboard remains responsive when upstream APIs fail.**

The WorldMonitor application aggregates data from numerous third-party sources including weather alerts, wildfire detections, and economic indicators. To prevent cascading failures and maintain UI responsiveness, the codebase implements robust **circuit breaker and fallback patterns** that isolate faulty dependencies and serve cached or default data when external APIs become unavailable.

## Core Circuit Breaker Implementation

The heart of the resilience strategy lives in **[`src/utils/circuit-breaker.ts`](https://github.com/koala73/worldmonitor/blob/main/src/utils/circuit-breaker.ts)**, which exports a factory function `createCircuitBreaker` that returns an executor object managing state transitions between closed, open, and half-open states.

### Failure Detection and State Management

The breaker tracks consecutive failures using an internal counter. When `state.failures` reaches the configurable `maxFailures` threshold, the breaker trips and enters a cooldown period.

```typescript
// src/utils/circuit-breaker.ts (lines 78-85)
if (error) {
  state.failures++;
  if (state.failures >= maxFailures) {
    state.cooldownUntil = Date.now() + cooldownMs;
    state.status = 'open';
  }
  throw error;
}

```

### Cooldown and Recovery Mechanisms

The `isOnCooldown()` method checks whether the current time is less than `state.cooldownUntil`. While the circuit is open, all incoming requests fail fast without hitting the network, reducing load on struggling upstream services.

```typescript
// src/utils/circuit-breaker.ts (lines 19-27)
function isOnCooldown(): boolean {
  if (state.cooldownUntil && Date.now() < state.cooldownUntil) {
    return true;
  }
  // Reset after cooldown expires
  if (state.cooldownUntil && Date.now() >= state.cooldownUntil) {
    state.failures = 0;
    state.cooldownUntil = null;
  }
  return false;
}

```

### Global Registry and Status Reporting

All breaker instances register themselves in a global `Map`, enabling the UI to query health status for any service via `getCircuitBreakerStatus()`. The `getStatus()` method returns human-readable strings such as `ok`, `temporarily unavailable`, or `offline mode`, which the frontend uses to render health indicators.

```typescript
// src/utils/circuit-breaker.ts (lines 33-43, 54-69)
function getStatus(): string {
  if (isOnCooldown()) return 'temporarily unavailable';
  if (navigator.onLine === false) return 'offline mode';
  return 'ok';
}

// Global registry
const registry = new Map<string, CircuitBreaker>();

export function getCircuitBreakerStatus() {
  const status: Record<string, string> = {};
  registry.forEach((breaker, name) => {
    status[name] = breaker.getStatus();
  });
  return status;
}

```

## Fallback Patterns and Caching Strategies

Beyond tripping the circuit, the utility implements six distinct **fallback patterns** that ensure data availability even when the upstream API is unreachable.

### In-Memory and Persistent Caching

Successful responses are cached in memory with a configurable `cacheTtlMs`. When `persistCache: true`, the breaker additionally serializes the cache entry to IndexedDB via `hydratePersistentCache()`, allowing data to survive page reloads or application restarts.

```typescript
// src/utils/circuit-breaker.ts (lines 49-56, 70-99)
function recordSuccess(data: T) {
  state.cache = {
    data,
    timestamp: Date.now(),
    ttl: cacheTtlMs
  };
  if (persistCache) {
    saveToIndexedDB(state.cache);
  }
}

async function hydratePersistentCache() {
  const saved = await loadFromIndexedDB();
  if (saved && Date.now() - saved.timestamp < cacheTtlMs) {
    state.cache = saved;
  }
}

```

### Stale-While-Revalidate (SWR) Pattern

If a cached entry exists but has exceeded its TTL, the breaker returns the stale payload immediately while triggering a background refresh. This eliminates loading states on every page load while keeping data relatively fresh.

```typescript
// src/utils/circuit-breaker.ts (lines 22-27)
function getCachedOrStale(): T | null {
  if (!state.cache) return null;
  const isExpired = Date.now() - state.cache.timestamp > state.cache.ttl;
  if (isExpired) {
    // Return stale but trigger background refresh
    setTimeout(() => execute(fetchFn), 0);
  }
  return state.cache.data;
}

```

### Default Value Fallbacks and Offline Detection

When the circuit is open and no cache is available, `execute` returns a caller-supplied `defaultValue`. For the Tauri desktop client, `isDesktopOfflineMode` checks `navigator.onLine` to prefer cached data and report a distinct offline status when the network is unavailable.

```typescript
// src/utils/circuit-breaker.ts (lines 6-8, 37-41)
async function execute(fetchFn: () => Promise<T>, defaultValue: T): Promise<T> {
  if (isOnCooldown()) {
    return state.cache?.data ?? defaultValue;
  }
  // ... fetch logic
}

function isDesktopOfflineMode(): boolean {
  return typeof navigator !== 'undefined' && navigator.onLine === false;
}

```

### Explicit Cache Clearing

Services can manually invalidate the cache by calling `breaker.clearCache()`, which wipes both the in-memory entry and the persistent IndexedDB record. This is useful for user-triggered refresh actions.

```typescript
// src/utils/circuit-breaker.ts (lines 70-77)
function clearCache() {
  state.cache = null;
  if (persistCache) {
    deleteFromIndexedDB();
  }
}

```

## Real-World Usage Examples

Every external data source in WorldMonitor instantiates a breaker with appropriate TTL and persistence settings, then calls `breaker.execute(fn, default)`.

### Weather Service with Persistent Caching

The National Weather Service integration uses a 30-minute cache that persists across restarts, ensuring users see alerts even after closing and reopening the app.

```typescript
// src/services/weather.ts
import { createCircuitBreaker } from '@/utils';

const breaker = createCircuitBreaker<WeatherAlert[]>({
  name: 'NWS Weather',
  cacheTtlMs: 30 * 60 * 1000,
  persistCache: true,
});

export async function fetchWeather(): Promise<WeatherAlert[]> {
  return breaker.execute(fetchFromNWS, []);
}

```

### Wildfire Detection Feed

The wildfire service follows the same pattern, caching high-value geospatial data for 30 minutes with persistence enabled.

```typescript
// src/services/wildfires/index.ts
const breaker = createCircuitBreaker<ListFireDetectionsResponse>({
  name: 'Wildfires',
  cacheTtlMs: 30 * 60 * 1000,
  persistCache: true,
});

```

### Real-Time Market Quotes

For request-specific financial data where caching provides no benefit, the breaker operates with `cacheTtlMs: 0`, functioning purely as a failure detector and circuit interrupter.

```typescript
// src/services/market/index.ts
const stockBreaker = createCircuitBreaker<ListMarketQuotesResponse>({ 
  name: 'Market Quotes', 
  cacheTtlMs: 0 
});

```

## Summary

WorldMonitor's **[`src/utils/circuit-breaker.ts`](https://github.com/koala73/worldmonitor/blob/main/src/utils/circuit-breaker.ts)** provides a comprehensive resilience layer that combines the **circuit breaker pattern** with multiple **fallback strategies** to handle external API failures gracefully.

- **Centralized state management** tracks failures, cooldown periods, and circuit status across all services via a global registry.
- **Multi-layered caching** includes in-memory TTL caching, stale-while-revalidate background refreshes, and IndexedDB persistence for offline survival.
- **Graceful degradation** returns cached stale data, persistent backups, or caller-supplied defaults when circuits are open or the network is offline.
- **Framework-agnostic design** allows any service to wrap external calls with `breaker.execute(fetchFn, defaultValue)`, ensuring consistent error handling without polluting business logic.

## Frequently Asked Questions

### How does the circuit breaker decide when to open the circuit?

The breaker increments an internal failure counter each time the wrapped function throws an error. When `state.failures` reaches the configured `maxFailures` threshold, the breaker sets `state.cooldownUntil` to the current time plus `cooldownMs` and transitions to the open state, as implemented in [`src/utils/circuit-breaker.ts`](https://github.com/koala73/worldmonitor/blob/main/src/utils/circuit-breaker.ts) lines 78-85.

### What happens when the circuit is open but no cached data exists?

If the circuit is on cooldown and the cache is empty, the `execute` method returns the `defaultValue` parameter supplied by the caller. This ensures that services always receive a predictable response—such as an empty array or placeholder object—rather than throwing exceptions, as shown in lines 6-8 of the circuit breaker implementation.

### How does WorldMonitor handle offline scenarios in the Tauri desktop client?

The breaker checks `navigator.onLine` via the `isDesktopOfflineMode` helper. When the network is unavailable, the breaker prefers cached data and reports a distinct "offline mode" status through `getStatus()`, allowing the UI to surface appropriate messaging while continuing to display stale data from IndexedDB or memory caches.

### Can developers manually invalidate the cache for a specific service?

Yes. Each breaker instance exposes a `clearCache()` method that wipes both the in-memory entry and the persistent IndexedDB record if `persistCache` is enabled. Services typically expose this as a "Refresh" button, allowing users to force a fresh network call and bypass stale data when needed.