The server acting as a gateway or proxy did not receive a timely response from the upstream server. Your request reached the proxy layer, but the backend server took too long to respond and the proxy gave up waiting.
HTTP 504 Gateway Timeout is a server error status code indicating that a server acting as a gateway or proxy did not receive a timely response from an upstream server. The proxy successfully connected to the backend but the backend did not send a complete response within the proxy's configured timeout window. This is different from 502 Bad Gateway, where the proxy could not establish a connection at all or received an invalid response.
In modern web infrastructure, 504 errors typically occur when the backend application is processing a request that takes longer than the reverse proxy or load balancer allows. Common scenarios include slow database queries, unresponsive external API calls, compute-intensive operations like report generation, and memory-constrained processes that are swapping to disk. The proxy has a configured timeout, often 30 to 60 seconds for web requests, and returns 504 when this timer expires.
The solution to 504 errors depends on whether the slow response is expected or problematic. If the backend legitimately needs more time for certain operations, the architectural solution is to use asynchronous processing. The server returns 202 Accepted immediately and processes the request in the background, providing a polling endpoint or webhook for the client to retrieve the result. If the slow response is unexpected, the solution is to optimize the backend by fixing slow queries, adding caches, or setting timeouts on external API calls.
The backend is executing a complex or unoptimized database query that takes longer than the proxy timeout. Large table scans, missing indexes, and N+1 query patterns are common culprits.
The backend is calling a third-party API that is slow or unresponsive. Without proper timeout settings, the backend waits indefinitely while the proxy timer expires.
The request triggers heavy computation such as report generation, image processing, or data transformation that exceeds the proxy timeout window.
The reverse proxy or load balancer timeout is set too low for the expected response time of the backend. Long-running but legitimate operations are cut short by the proxy timer.
Use database EXPLAIN plans to identify slow queries. Add appropriate indexes, optimize query structure, and avoid N+1 patterns. Consider caching frequently accessed data to eliminate repeated queries.
Add explicit timeouts to all external HTTP calls in your backend code. If a third-party API is slow, use circuit breaker patterns to fail fast rather than holding the request open.
If the backend legitimately needs more time, increase the proxy_read_timeout in Nginx or the equivalent setting in your load balancer. Be cautious, as very long timeouts tie up proxy connections.
For long-running operations, return 202 Accepted immediately and process the request asynchronously. Provide a polling endpoint or webhook for clients to check the result when it is ready.
// Express.js — timeout handling and async processing pattern
const express = require('express');
const app = express();
// Set server-level timeout
app.use((req, res, next) => {
// Set a 30-second timeout for all routes
req.setTimeout(30000, () => {
if (!res.headersSent) {
res.status(504).json({
error: 'Gateway Timeout',
message: 'Request took too long to process'
});
}
});
next();
});
// Async processing for long-running tasks
app.post('/api/reports', async (req, res) => {
const jobId = generateJobId();
// Start processing in the background
processReportAsync(jobId, req.body).catch(err => {
console.error(`Report job ${jobId} failed:`, err);
});
// Return 202 immediately — do not wait for completion
res.status(202).json({
jobId,
status: 'processing',
statusUrl: `/api/reports/${jobId}/status`
});
});
// Poll for job completion
app.get('/api/reports/:jobId/status', async (req, res) => {
const job = await getJob(req.params.jobId);
if (!job) return res.status(404).json({ error: 'Job not found' });
if (job.status === 'completed') {
return res.json({
status: 'completed',
downloadUrl: job.resultUrl
});
}
res.json({ status: job.status, progress: job.progress });
});
# Flask — timeout handling and async processing pattern
from flask import Flask, jsonify, request
import uuid
import threading
app = Flask(__name__)
jobs = {}
# Async processing for long-running tasks
@app.route('/api/reports', methods=['POST'])
def create_report():
job_id = str(uuid.uuid4())
jobs[job_id] = {'status': 'processing', 'progress': 0}
# Start processing in a background thread
thread = threading.Thread(
target=process_report,
args=(job_id, request.get_json())
)
thread.start()
# Return 202 immediately
return jsonify(
job_id=job_id,
status='processing',
status_url=f'/api/reports/{job_id}/status'
), 202
def process_report(job_id, data):
try:
# ... long-running report generation
result_url = generate_report(data)
jobs[job_id] = {'status': 'completed', 'result_url': result_url}
except Exception as e:
jobs[job_id] = {'status': 'failed', 'error': str(e)}
# Poll for job completion
@app.route('/api/reports/<job_id>/status', methods=['GET'])
def report_status(job_id):
job = jobs.get(job_id)
if not job:
return jsonify(error='Job not found'), 404
return jsonify(**job)
HTTP 504 means the proxy connected to the upstream server but it did not respond before the timeout expired. HTTP 502 means the proxy could not connect to the upstream or received an invalid response. 504 is a timeout issue; 502 is a connection or response issue.
Increasing the proxy timeout may resolve the 504 but is usually the wrong fix. It keeps connections open longer, consuming resources. Fix the root cause by optimizing slow queries, adding caches, or moving long-running tasks to async processing.
Serverless platforms like AWS Lambda, Vercel, and Cloudflare Workers have execution time limits. If your function exceeds this limit, the platform's gateway returns 504. Optimize your function or move long-running work to a different architecture.
Yes, but with caution. If the request is idempotent (GET, PUT, DELETE), retry with exponential backoff. If the request is not idempotent (POST), retry only if you can guarantee the operation was not partially completed on the server.
Add indexes to frequently queried columns, use EXPLAIN to identify full table scans, implement query result caching, use connection pooling, and set query timeout limits in your database driver configuration.
Get instant alerts when your endpoints go down. 60-second checks, free forever.
Start Monitoring Free →