This is a common issue when using Laravel Horizon (and queue workers in general), and it comes down to how PHP process management and job timeouts work in Laravel.
Why does this happen?
The timeout you set (in Horizon config and as a CLI argument) only applies to how long the worker process will allow a job to run before sending a SIGKILL (kill signal) to the PHP process. However, there are a few caveats:
-
Timeout is not always enforced immediately:
The worker checks the timeout, but if the job is running PHP code that doesn't check for signals (e.g., a long-running loop, or a blocking call to an external service), the process may not respond to the kill signal right away.
-
PCNTL Extension:
Laravel uses the pcntl extension to handle timeouts. If your PHP is not running with pcntl enabled (common on some servers, especially Windows), the timeout cannot be enforced at all.
-
Long-running system calls:
If your job is making a blocking call (like a long cURL request, or waiting on a database), the process may not be able to handle the timeout signal until the call completes.
-
Supervisor/Process Management:
If you're running Horizon under a process manager (like Supervisor), and the worker process gets stuck, it may not be killed and restarted as expected.
Why do you see jobs running for 800+ seconds?
- The job started, and for some reason (e.g., a blocking call, or pcntl not enabled), the worker process did not kill the job after 120 seconds.
- Eventually, the job finished or was killed, but by then, the runtime was much longer than the configured timeout.
- Horizon records the actual runtime, not just the configured timeout.
How to fix or mitigate
1. Ensure pcntl is enabled
Check your PHP installation:
php -m | grep pcntl
If it's not listed, install/enable it (on Ubuntu: sudo apt-get install php-pcntl).
2. Avoid blocking calls in jobs
If your job does things like:
- Long cURL requests without a timeout
- Waiting for external APIs
- Long database queries
Make sure to set timeouts on those operations.
3. Use process managers correctly
If using Supervisor, make sure your configuration allows for process restarts if they hang.
4. Upgrade Laravel/Horizon
You're on Laravel 5.8 and Horizon 3.7.2, which are quite old. There have been many improvements and bugfixes in newer versions regarding job handling and timeouts.
5. Set lower retry_after
Your retry_after is 125 seconds, which is just above your timeout (120). It's recommended to set retry_after to be greater than the maximum expected job runtime, but not too close to the timeout, to avoid jobs being released back to the queue while still running.
6. Graceful job interruption
If possible, refactor your job code to periodically check for signals or timeouts, and exit gracefully.
Summary
The most likely reason is that the PHP process running your job is not being killed after the timeout, due to either blocking code or missing pcntl support.
Check your PHP extensions, avoid blocking calls, and consider upgrading Laravel/Horizon for better reliability.
References
Example: Setting a timeout on a Guzzle HTTP request in your job
$client = new \GuzzleHttp\Client([
'timeout' => 10, // seconds
]);
$response = $client->get('https://example.com/api');
If you provide more details about what your job is doing, I can give more targeted advice!