logo

API Timeout Troubleshooting Guide

Your API is returning 504s, requests are hanging, and users are complaining. Timeouts are difficult to debug because the cause can lurk anywhere in the chain — your code, your database, a third-party service, or your infrastructure.

This guide walks you through diagnosing and fixing the most common timeout causes.

Step 1: Identify the type of timeout

Not all timeouts are the same. The error you’re seeing reveals the failure point:

ErrorStatus CodeWhat It Means
ETIMEDOUTConnection could not be established
ESOCKETTIMEDOUTConnection established but response took too long
504 Gateway Timeout504Reverse proxy (Nginx, Cloudflare) gave up waiting
408 Request Timeout408Server closed the connection due to idle client
ERR_NETWORK / AbortErrorClient (browser/fetch) cancelled the request
// Check what you're actually getting
app.use((err, req, res, next) => {
  if (err.code === 'ETIMEDOUT') {
    console.error('Connection timeout to:', err.address);
  } else if (err.code === 'ESOCKETTIMEDOUT') {
    console.error('Read timeout — server connected but response too slow');
  }
  next(err);
});

504 Gateway Timeout is the most common in production. It means your reverse proxy or platform (Vercel, AWS ALB, Cloudflare) enforces a time limit that your endpoint exceeded.

Default timeout limits by platform:

PlatformDefault Timeout
Vercel (Hobby)10s
Vercel (Pro)60s
AWS ALB60s
Cloudflare100s
Nginx60s
Heroku30s

Step 2: Trace the slow path

Before fixing anything, find the bottleneck. Add timing to your endpoint:

app.post('/api/generate-invoice', async (req, res) => {
  const start = Date.now();
  const timers = {};

  timers.dbQuery = Date.now();
  const orders = await db.orders.findMany({ where: { userId: req.userId } });
  timers.dbQuery = Date.now() - timers.dbQuery;

  timers.calculation = Date.now();
  const invoice = calculateInvoice(orders);
  timers.calculation = Date.now() - timers.calculation;

  timers.pdfGeneration = Date.now();
  const pdf = await generatePDF(invoice);
  timers.pdfGeneration = Date.now() - timers.pdfGeneration;

  timers.emailSend = Date.now();
  await sendEmail(req.userId, pdf);
  timers.emailSend = Date.now() - timers.emailSend;

  console.log('Endpoint timing:', {
    total: Date.now() - start,
    ...timers,
  });

  res.json({ invoiceId: invoice.id });
});

Common culprits:

  • Unbounded database queries returning thousands of rows
  • N+1 queries making hundreds of sequential database calls
  • External API calls to slow or unresponsive services
  • Heavy computation like PDF generation, image processing, or data aggregation on the server
  • Missing connection pools creating a new connection per request

Step 3: Apply quick fixes for common causes

Missing connection pool

// Bad: new connection every request
app.get('/api/data', async (req, res) => {
  const client = new Client(DATABASE_URL); // cold connection = 50-200ms
  await client.connect();
  const result = await client.query('SELECT ...');
  res.json(result.rows);
});

// Good: reuse connections
const pool = new Pool({ connectionString: DATABASE_URL, max: 20 });

app.get('/api/data', async (req, res) => {
  const result = await pool.query('SELECT ...');
  res.json(result.rows);
});

Unbounded queries

// Bad: could return millions of rows
const users = await db.users.findMany();

// Good: always paginate
const users = await db.users.findMany({ take: 50, skip: offset });

Missing timeouts on outbound calls

// Bad: waits forever for third-party API
const data = await fetch('https://slow-api.example.com/data');

// Good: fail fast
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

const data = await fetch('https://slow-api.example.com/data', {
  signal: controller.signal,
});
clearTimeout(timeout);

Synchronous operations blocking the event loop

// Bad: blocks the entire Node.js process
const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512');

// Good: non-blocking
const hash = await crypto.pbkdf2(password, salt, 100000, 64, 'sha512');

Step 4: Offload slow operations to a background queue

If an operation inherently takes a long time (report generation, data exports, AI inference), no optimization will squeeze it into a 30-second window. Move it out of the request cycle:

// Before: synchronous, times out
app.post('/api/export', async (req, res) => {
  const data = await aggregateAllUserData(req.userId);  // 30-120s
  const csv = await generateCSV(data);                   // 10-30s
  await uploadToS3(csv);                                 // 2-5s
  res.json({ downloadUrl: csv.url });                    // too late — already timed out
});

// After: instant response, background processing
app.post('/api/export', async (req, res) => {
  const exportId = crypto.randomUUID();

  await saveToDatabase({ id: exportId, status: 'pending', userId: req.userId });

  await aq.tasks.create({
    callbackUrl: 'https://your-app.com/api/run-export',
    payload: { exportId, userId: req.userId },
    webhookUrl: 'https://your-app.com/api/on-export-done',
    retries: 2,
    timeout: 300,
  });

  res.json({ exportId, status: 'pending' });
});

With AsyncQueue handling the slow work, your endpoint responds instantly while processing happens outside the request timeout window. Failed tasks get automatic retries.

Step 5: Set up monitoring and alerts

Once you’ve fixed the immediate issue, prevent future regressions:

// Track response times per endpoint
app.use((req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    const route = req.route?.path || req.path;

    if (duration > 5000) {
      console.warn(`Slow endpoint: ${req.method} ${route} took ${duration}ms`);
    }

    metrics.histogram('http_request_duration_ms', duration, {
      method: req.method,
      route,
      status: res.statusCode,
    });
  });

  next();
});

Key metrics to track:

  • p95 and p99 response times — averages mask tail latency
  • Timeout rate — percentage of requests returning 504/408
  • External API response times — track each third-party dependency separately
  • Queue depth — if using background tasks, watch for backlog buildup

Quick Reference: Timeout Checklist

  1. Check the error code — is it a connection, read, or gateway timeout?
  2. Add timing logs to find the slow operation
  3. Look for missing connection pools, unbounded queries, and missing outbound timeouts
  4. If the operation is inherently slow, offload it to AsyncQueue
  5. Add response time monitoring to catch future slowdowns