Fix API Timeouts: Retry and Fallback Patterns | OpsBlu Docs

Fix API Timeouts: Retry and Fallback Patterns

How to diagnose and fix API timeout failures. Covers timeout budgets, AbortController patterns, retry strategies with exponential backoff, and...

What This Means

Without proper timeout handling, API requests can hang indefinitely, causing:

  • Frozen UI elements
  • Memory leaks
  • Poor user experience
  • Wasted resources on failed requests

How to Diagnose

Identify Hanging Requests

DevTools > Network:

  1. Look for requests with "Pending" status for long periods
  2. Check for requests without response
  3. Filter by time to find slow requests

Console Monitoring

// Log slow requests
const originalFetch = window.fetch;
window.fetch = async function(...args) {
  const start = performance.now();
  try {
    const response = await originalFetch.apply(this, args);
    const duration = performance.now() - start;
    if (duration > 3000) {
      console.warn(`Slow request (${duration}ms):`, args[0]);
    }
    return response;
  } catch (error) {
    console.error('Request failed:', args[0], error);
    throw error;
  }
};

General Fixes

Fetch with AbortController

// Basic timeout implementation
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });
    clearTimeout(timeoutId);
    return response;
  } catch (error) {
    clearTimeout(timeoutId);

    if (error.name === 'AbortError') {
      throw new Error(`Request timed out after ${timeout}ms`);
    }
    throw error;
  }
}

// Usage
try {
  const response = await fetchWithTimeout('/api/data', {}, 5000);
  const data = await response.json();
} catch (error) {
  if (error.message.includes('timed out')) {
    showTimeoutMessage();
  }
}

Configurable Timeout Utility

class ApiClient {
  constructor(baseUrl, defaultTimeout = 10000) {
    this.baseUrl = baseUrl;
    this.defaultTimeout = defaultTimeout;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseUrl}${endpoint}`;
    const timeout = options.timeout || this.defaultTimeout;
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    const config = {
      ...options,
      signal: controller.signal,
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      }
    };

    try {
      const response = await fetch(url, config);
      clearTimeout(timeoutId);

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      return await response.json();
    } catch (error) {
      clearTimeout(timeoutId);

      if (error.name === 'AbortError') {
        throw new TimeoutError(url, timeout);
      }
      throw error;
    }
  }

  get(endpoint, options) {
    return this.request(endpoint, { ...options, method: 'GET' });
  }

  post(endpoint, data, options) {
    return this.request(endpoint, {
      ...options,
      method: 'POST',
      body: JSON.stringify(data)
    });
  }
}

class TimeoutError extends Error {
  constructor(url, timeout) {
    super(`Request to ${url} timed out after ${timeout}ms`);
    this.name = 'TimeoutError';
    this.url = url;
    this.timeout = timeout;
  }
}

// Usage
const api = new ApiClient('https://api.example.com', 5000);
const data = await api.get('/users', { timeout: 3000 });

Axios Timeout

import axios from 'axios';

const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000, // 5 seconds
  timeoutErrorMessage: 'Request timed out'
});

// Per-request timeout
const response = await api.get('/users', { timeout: 3000 });

// Handle timeout errors
api.interceptors.response.use(
  response => response,
  error => {
    if (error.code === 'ECONNABORTED') {
      // Timeout error
      return Promise.reject(new Error('Request timed out'));
    }
    return Promise.reject(error);
  }
);

React Query with Timeout

import { useQuery } from '@tanstack/react-query';

async function fetchWithTimeout(url) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 5000);

  const response = await fetch(url, { signal: controller.signal });
  clearTimeout(timeoutId);
  return response.json();
}

function UserData() {
  const { data, error, isLoading } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetchWithTimeout('/api/users'),
    retry: (failureCount, error) => {
      // Don't retry on timeout
      if (error.name === 'AbortError') return false;
      return failureCount < 3;
    }
  });

  if (isLoading) return <Loading />;
  if (error) return <Error message={error.message} />;
  return <UserList users={data} />;
}

Progressive Timeout Strategy

// Increase timeout for retries
async function fetchWithProgressiveTimeout(url, options = {}) {
  const timeouts = [2000, 5000, 10000]; // Progressive timeouts
  let lastError;

  for (let i = 0; i < timeouts.length; i++) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeouts[i]);

    try {
      const response = await fetch(url, {
        ...options,
        signal: controller.signal
      });
      clearTimeout(timeoutId);
      return response;
    } catch (error) {
      clearTimeout(timeoutId);
      lastError = error;

      if (error.name === 'AbortError') {
        console.log(`Attempt ${i + 1} timed out after ${timeouts[i]}ms`);
        continue; // Try next timeout
      }
      throw error; // Non-timeout error, don't retry
    }
  }

  throw new Error(`All attempts failed. Last error: ${lastError.message}`);
}

UI Feedback for Timeouts

function LoadingWithTimeout({ timeout = 5000, onTimeout }) {
  const [showSlowMessage, setShowSlowMessage] = useState(false);

  useEffect(() => {
    const timer = setTimeout(() => {
      setShowSlowMessage(true);
    }, timeout / 2);

    const timeoutTimer = setTimeout(() => {
      onTimeout?.();
    }, timeout);

    return () => {
      clearTimeout(timer);
      clearTimeout(timeoutTimer);
    };
  }, [timeout, onTimeout]);

  return (
    <div className="loading">
      <Spinner />
      {showSlowMessage && (
        <p>This is taking longer than usual...</p>
      )}
    </div>
  );
}

Verification

  1. Test with Network throttling in DevTools
  2. Verify timeout errors are caught
  3. Check UI shows appropriate feedback
  4. Confirm no hung requests in Network tab

Further Reading