Table of Contents
Are you struggling with timeouts in your Laravel application when handling time-consuming processes? Youโre not alone. Many developers face this challenge, especially when dealing with services like Cloudflare that impose strict request timeouts.
In this comprehensive guide, weโll explore an efficient solution using PHPโs exec() function and the Linux screen command to run long-running tasks asynchronously.

The Challenge: Timeouts in Web Applications
Laravel applications often need to perform tasks that can take a significant amount of time to complete. These might include:
- Processing large datasets
- Generating complex reports
- Handling video processing
Running these tasks directly in a web request can lead to timeouts, resulting in errors like the dreaded โ504 Gateway Timeoutโ. This not only disrupts the user experience but can also leave your application in an inconsistent state.
The Solution: Asynchronous Execution with exec() and screen
To overcome this challenge, weโll leverage two powerful tools:
- PHPโs exec() function
- The Linux screen command
This combination allows us to offload long-running tasks to the background, ensuring quick completion of the main web request while the heavy lifting continues asynchronously.
Step-by-Step Implementation
1. Understanding the exec() Function
The exec() function in PHP executes an external program. Its syntax is:
exec(string $command, array &$output = null, int &$result_code = null): string
- $command: The command to execute
- $output: Optional parameter to capture the commandโs output
- $result_code: Optional parameter to capture the commandโs return code
2. Utilizing the screen Command
screen is a Linux utility that runs processes in separate terminal sessions. Itโs perfect for long-running processes as they continue even if you disconnect from the terminal or restart the web server.
Basic usage:
screen -dmS session_name commandThis starts a new detached screen session with the specified name and runs the given command.
3. Implementing in Laravel
Letโs create a controller to trigger our long-running process:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class BackgroundProcessController extends Controller
{
public function startProcess()
{
$basePath = base_path();
$command = 'screen -d -m /usr/bin/php {$basePath}/artisan process:run';
exec($command, $output, $resultCode);
if ($resultCode === 0) {
return response()->json(['message' => 'Process started successfully in the background'], 200);
} else {
return response()->json(['message' => 'Failed to start process', 'output' => $output, 'resultCode' => $resultCode], 500);
}
}
}This controller method: 1. Composes a command to start a new screen session and run an Artisan command 2. Executes the command using exec() 3. Returns a JSON response indicating the process status
4. Creating the Long-Running Artisan Command
Generate a new Artisan command:
php artisan make:command RunProcessUpdate the RunProcess command class:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class RunProcess extends Command
{
protected $signature = 'process:run';
protected $description = 'Run a long-running process';
public function handle()
{
sleep(300); // Simulating a 5-minute process
// Your long-running logic goes here
$this->info('Long-running process completed!');
}
}This command simulates a long-running task with a 5-minute sleep. In a real-world scenario, replace this with your actual business logic.
Enhancing User Experience with Real-Time Updates
While running tasks asynchronously solves the timeout problem, it introduces a new challenge: how do we inform the user when the task is complete? Letโs enhance our solution by adding real-time updates using Laravelโs event system and Laravel Echo.
1. Dispatching an Event on Process Completion
First, letโs modify our RunProcess command to dispatch an event when the long-running process is complete:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Events\LongRunningProcessCompleted;
class RunProcess extends Command
{
protected $signature = 'process:run';
protected $description = 'Run a long-running process';
public function handle()
{
// Simulate a long-running task
sleep(300);
// Your long-running logic goes here
// Dispatch an event when the process is complete
event(new LongRunningProcessCompleted('Process completed successfully!'));
$this->info('Long-running process completed!');
}
}2. Creating the Event
Next, letโs create the LongRunningProcessCompleted event:
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class LongRunningProcessCompleted implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $message;
public function __construct($message)
{
$this->message = $message;
}
public function broadcastOn()
{
return new Channel('process-updates');
}
}This event will be broadcast on the โprocess-updatesโ channel.
3. Listening for the Event with Laravel Echo
Now, letโs set up the front end to listen to this event using Laravel Echo. Add the following JavaScript to your application:
import Echo from 'laravel-echo';
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'your-pusher-key',
cluster: 'your-pusher-cluster',
encrypted: true
});
Echo.channel('process-updates')
.listen('LongRunningProcessCompleted', (e) => {
console.log(e.message);
// Update your UI here
alert('Process completed: ' + e.message);
});This code sets up Laravel Echo to listen on the โprocess-updatesโ channel for the LongRunningProcessCompleted event. When the event is received, it logs the message to the console and shows an alert. In a real application, youโd update your UI more gracefully.
4. Updating the Controller
Finally, letโs update our BackgroundProcessController to inform the user that theyโll receive real-time updates:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class BackgroundProcessController extends Controller
{
public function startProcess()
{
$basePath = base_path();
$command = 'screen -d -m /usr/bin/php {$basePath}/artisan process:run';
exec($command, $output, $resultCode);
if ($resultCode === 0) {
return response()->json([
'message' => 'Process started successfully in the background. You will receive a notification when it\'s complete.'
], 200);
} else {
return response()->json([
'message' => 'Failed to start process',
'output' => $output,
'resultCode' => $resultCode
], 500);
}
}
}Alternative Approach: Using Laravelโs Queue System
While the exec() and screen method provides a robust solution for running long processes, Laravelโs built-in queue system offers another powerful approach. Hereโs a quick overview of how you can use the Artisan Facade to dispatch a job to a high-priority queue channel:
Using Artisan Facade with Queue
You can modify your BackgroundProcessController to use Laravelโs queue system instead of exec() and screen:
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Http\Request;
class BackgroundProcessController extends Controller
{
public function startProcess()
{
Artisan::queue('process:run')->onQueue('high');
return response()->json([
'message' => 'Process queued successfully. You will receive a notification when it\'s complete.'
], 200);
}
}In this approach:
- We use
Artisan::queue()it insteadexec()to run our command. - The
onQueue('high')method specifies that this job should be processed on the โhighโ priority queue. - Laravelโs queue worker will handle running the job in the background.
This method leverages Laravelโs queue system, which provides benefits like automatic retries, job prioritization, and easier monitoring. However, it requires setting up and running queue workers, which might add complexity to your server setup compared to the screen method.
Choose the approach that best fits your applicationโs needs and your teamโs familiarity with Laravelโs queue system.
Conclusion
By implementing this asynchronous execution strategy with real-time updates, youโve not only solved the timeout problem but also significantly enhanced the user experience. Your Laravel application can now handle long-running tasks efficiently while keeping users informed of the process status in real-time.
This powerful combination of background processing and real-time communication opens up new possibilities for your applications. Whether youโre processing large datasets, generating complex reports, or handling any resource-intensive operations, you can now do so without compromising on user experience or application performance.
Implement this strategy today and watch your Laravel application handle long-running processes with ease while keeping your users engaged and informed!
Remember to properly set up Laravel Echo and a compatible web socket server (like Pusher or Laravel Reverb) in your production environment to ensure the real-time functionality works as expected.