[ GUIDE ]

How to debug Laravel timeouts

Where each layer fails (PHP, PHP-FPM, Nginx, database), how to tell them apart, and how to fix without just raising every timeout.

QUICK ANSWER

How do I debug a Laravel 504 timeout?

Identify the layer that gave up first. Check Nginx access logs for the 504 — you'll see URL, timestamp, and upstream_response_time. If upstream_response_time is near fastcgi_read_timeout, Nginx dropped. If near request_terminate_timeout, PHP-FPM killed the worker. If it's max_execution_time, PHP timed itself out. The layer tells you where to look — most 504s are ultimately a slow database query or external HTTP call, visible in an APM's per-request trace.

Updated · 2026-06-06

A timeout is the least helpful error your app can produce, because the place it surfaces is almost never the place it's caused. Nginx returns the 504, but Nginx didn't do anything wrong — it just got tired of waiting for PHP, which got tired of waiting for a query, which got tired of waiting for a lock. Debugging a timeout is really an exercise in walking that chain backwards to the one layer that actually stalled, and the trick is knowing how each layer signals that it was the one that gave up first.

The five timeout layers

A Laravel HTTP request touches multiple layers. Each has its own timeout:

Layer Default Config
Client (browser, curl)Varies (~30s)Client-specific
Load balancer (ALB, CF)60s AWS ALB defaultidle_timeout
Nginx upstream60sfastcgi_read_timeout
PHP-FPM request0 (unlimited)request_terminate_timeout
PHP script30smax_execution_time
Database statementUnlimited (dangerous)statement_timeout (Postgres)

Diagnose the layer that failed

Nginx access log

bash
# Enable timing fields in log_format
log_format upstream_timing '$remote_addr $request $status '
                           'request=$request_time upstream=$upstream_response_time';

# Look for 504s with timing
grep ' 504 ' /var/log/nginx/access.log | tail

If upstream_response_time is ~equal to fastcgi_read_timeout, Nginx gave up. If upstream_response_time is -, PHP-FPM didn't respond at all (crashed, full worker pool, or no workers running).

PHP-FPM slowlog

/etc/php/8.2/fpm/pool.d/www.conf

ini
slowlog = /var/log/php-fpm/slow.log
request_slowlog_timeout = 10s
request_terminate_timeout = 60s

PHP-FPM dumps the stack trace of any request taking longer than request_slowlog_timeout. Priceless for catching where the hang actually happens — often inside a database driver or file operation.

Database-level timeouts

config/database.php (PostgreSQL)

php
'pgsql' => [
    'driver' => 'pgsql',
    // ...
    'options' => [
        PDO::ATTR_TIMEOUT => 5,
    ],
    // Per-session statement timeout
    'statement_timeout' => '15s',
],

Without statement_timeout, a query can hold a PHP-FPM worker indefinitely. Set a value lower than your request timeout so DB hangs fail recoverably instead of taking out the worker.

Common 504 causes

  1. Synchronous external HTTP call with no timeout. Default Guzzle = unlimited. Set Http::timeout(5) on every external call.
  2. Slow DB query on unindexed column. EXPLAIN ANALYZE reveals full-table scans.
  3. Exhausted PHP-FPM worker pool. pm.max_children too low, legitimate requests queue, queue exceeds timeout.
  4. Lock contention on a hot row. SELECT ... FOR UPDATE blocking other requests. Visible in Postgres's pg_locks.
  5. Blocking cron job on the same server. A scheduled task hogging CPU or disk starves web workers.

Don't just raise every timeout

The instinct to bump fastcgi_read_timeout to 300s papers over the real issue. A request taking 300s shouldn't run in a request at all — queue it. Use a synchronous request for the queue dispatch and return a 202 with a polling URL or a webhook subscription.

THE EASY WAY

NightOwl surfaces slow and timed-out requests with trace drilldown

Every request is recorded with duration, status, and full per-span breakdown. Filter by 504 or slow duration to find timeouts; click a request to see the dominant span — usually the DB query or external HTTP call that hung.

bash
composer require nightowl/agent
php artisan nightowl:install

From $5/month flat. Data in your PostgreSQL.

Frequently asked questions

Why am I getting 504 Gateway Timeout errors in Laravel?

Something in the request took longer than your web server's upstream timeout. Common culprits: a hung database query, a slow external API called synchronously, a PHP-FPM worker exhausted and queuing requests, or max_execution_time hit. The 504 is the symptom — the trace of the affected request shows which span actually took the time.

What's the default PHP timeout in Laravel?

max_execution_time defaults to 30 seconds for HTTP requests in most php.ini configurations. CLI is unlimited by default. Laravel doesn't override this unless you do it explicitly with set_time_limit() or ini_set(). Nginx and PHP-FPM each have their own timeouts — request_terminate_timeout in PHP-FPM, fastcgi_read_timeout in Nginx — which must all be compatible.

How do I increase the Laravel request timeout?

Three layers, all must increase together. PHP: set_time_limit(120) or ini_set('max_execution_time', 120). PHP-FPM: request_terminate_timeout = 120. Nginx: fastcgi_read_timeout 120s. If any one is lower, requests still timeout at that layer. For most apps, a better answer is to not run long work in a request — use queues.

What's the difference between 504 and 502 in Laravel?

504 Gateway Timeout — upstream (PHP-FPM) didn't respond within the timeout. The request was in progress but too slow. 502 Bad Gateway — upstream crashed or closed the connection. Usually a PHP fatal error, worker crash, or PHP-FPM running out of worker slots. Both surface from Nginx; their causes differ.

How do I find which request timed out in Laravel?

Check Nginx's access log for 504 status codes — the URL and timestamp give you the request. Then check PHP-FPM's slowlog (configure with request_slowlog_timeout) to see the PHP stack trace at the moment of the timeout. Or use an APM that records every request with duration and status — slow + 504 requests surface directly.

Why do Laravel queue jobs time out?

Two different timeouts apply. Laravel's per-job timeout (timeout property on the job class, default 60s) — the worker kills the child process after this. PHP's max_execution_time on the worker process itself. If your job exceeds Laravel's timeout, it's killed and marked released; if PHP-level times out first (unusual, would need to be lower), the job fails with a fatal error.

How do I prevent database queries from hanging Laravel requests?

Set statement_timeout in PostgreSQL or max_execution_time in MySQL on the connection. In Laravel, configure this via the connection options in config/database.php. A query that exceeds the timeout errors with a recoverable exception instead of hanging the PHP-FPM worker indefinitely.

What's the right PHP-FPM pm.max_children setting?

Roughly: (available_memory - overhead) / average_process_memory. For an 8GB server with 100MB per process and ~2GB overhead, that's (8000-2000)/100 = 60 children. Set too low and requests queue and 504. Set too high and workers swap memory and everything slows down. Monitor with pm.status_path and tune from data, not guesses.

PRICING

Flat pricing. No event caps. No per-seat fees.

14-day free trial, no credit card. Your PostgreSQL, your data.

HOBBY

$5 /month

1 app · 14 days lookback · all Laravel events

TEAM

$15 /month

Up to 3 connected apps · unlimited environments · all Laravel events

AGENCY

$69 /month

Unlimited apps · unlimited agent instances · same flat rate at any traffic

Related