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:
| Error | Status Code | What It Means |
|---|---|---|
ETIMEDOUT | — | Connection could not be established |
ESOCKETTIMEDOUT | — | Connection established but response took too long |
504 Gateway Timeout | 504 | Reverse proxy (Nginx, Cloudflare) gave up waiting |
408 Request Timeout | 408 | Server closed the connection due to idle client |
ERR_NETWORK / AbortError | — | Client (browser/fetch) cancelled the request |
// Check what you're actually gettingapp.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:
| Platform | Default Timeout |
|---|---|
| Vercel (Hobby) | 10s |
| Vercel (Pro) | 60s |
| AWS ALB | 60s |
| Cloudflare | 100s |
| Nginx | 60s |
| Heroku | 30s |
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 requestapp.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 connectionsconst 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 rowsconst users = await db.users.findMany();
// Good: always paginateconst users = await db.users.findMany({ take: 50, skip: offset });Missing timeouts on outbound calls
// Bad: waits forever for third-party APIconst data = await fetch('https://slow-api.example.com/data');
// Good: fail fastconst 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 processconst hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512');
// Good: non-blockingconst 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 outapp.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 processingapp.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 Vunr 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 endpointapp.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
- Check the error code — is it a connection, read, or gateway timeout?
- Add timing logs to find the slow operation
- Look for missing connection pools, unbounded queries, and missing outbound timeouts
- If the operation is inherently slow, offload it to Vunr
- Add response time monitoring to catch future slowdowns