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

WorldMonitor uses a centralized circuit-breaker utility in 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, 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.

// 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.

// 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.

// 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.

// 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.

// 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.

// 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.

// 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.

// 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.

// 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.

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

Summary

WorldMonitor's 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →