| title | Queue Workers Guide |
|---|---|
| description | Running background jobs with Laravel queues, Horizon, and PHP workers |
| weight | 5 |
Run background jobs reliably with Cbox images.
Use the php-cli image with process env vars. The entrypoint auto-starts Cbox Init with structured logging, metrics, health checks, and graceful shutdown:
# docker-compose.yml
services:
worker:
image: ghcr.io/cboxdk/php-baseimages/php-cli:8.4-bookworm
volumes:
- ./:/var/www/html
environment:
LARAVEL_QUEUE: "true"
QUEUE_CONNECTION: redis
REDIS_HOST: redis
depends_on:
- redis
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
redis_data:Scale workers within a single container using CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE:
worker:
image: ghcr.io/cboxdk/php-baseimages/php-cli:8.4-bookworm
environment:
LARAVEL_QUEUE: "true"
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE: "5"Add a high-priority queue with LARAVEL_QUEUE_HIGH:
worker:
image: ghcr.io/cboxdk/php-baseimages/php-cli:8.4-bookworm
environment:
LARAVEL_QUEUE: "true"
LARAVEL_QUEUE_HIGH: "true"
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE: "3"
CBOX_INIT_PROCESS_QUEUE_HIGH_SCALE: "2"If you need full control over the queue:work arguments, you can bypass Cbox Init with a direct command override. This loses structured logging, metrics, health checks, and graceful shutdown:
worker:
image: ghcr.io/cboxdk/php-baseimages/php-cli:8.4-bookworm
command: php artisan queue:work redis --sleep=3 --tries=3
volumes:
- ./:/var/www/html
environment:
QUEUE_CONNECTION: redis
REDIS_HOST: redisSimple worker for processing jobs:
worker:
image: ghcr.io/cboxdk/php-baseimages/php-cli:8.4-bookworm
environment:
LARAVEL_QUEUE: "true"
restart: unless-stoppedCbox Init manages restarts, memory limits, and graceful shutdown automatically. To customize queue:work arguments, use a direct command override instead:
worker:
image: ghcr.io/cboxdk/php-baseimages/php-cli:8.4-bookworm
command: php artisan queue:work redis --sleep=3 --tries=3 --max-jobs=1000 --max-time=3600
restart: unless-stoppedOptions explained (direct command only):
--sleep=3: Wait 3 seconds when no jobs--tries=3: Retry failed jobs 3 times--max-jobs=1000: Restart after 1000 jobs (prevents memory leaks)--max-time=3600: Restart after 1 hour
Comprehensive queue management with dashboard:
horizon:
image: ghcr.io/cboxdk/php-baseimages/php-cli:8.4-bookworm
restart: unless-stopped
volumes:
- ./:/var/www/html
environment:
LARAVEL_HORIZON: "true"
QUEUE_CONNECTION: redis
REDIS_HOST: redisAccess dashboard at /horizon after installing:
composer require laravel/horizon
php artisan horizon:install
php artisan migrateRun Laravel scheduled tasks:
scheduler:
image: ghcr.io/cboxdk/php-baseimages/php-cli:8.4-bookworm
restart: unless-stopped
environment:
LARAVEL_SCHEDULER: "true"Cbox Init runs schedule:work with structured logging, metrics, and graceful shutdown.
Alternative: Direct Command (loses Cbox Init features):
scheduler:
image: ghcr.io/cboxdk/php-baseimages/php-cli:8.4-bookworm
command: >
sh -c "while true; do
php artisan schedule:run --verbose --no-interaction
sleep 60
done"
restart: unless-stoppedservices:
app:
image: ghcr.io/cboxdk/php-baseimages/php-fpm-nginx:8.4-bookworm
worker:
image: ghcr.io/cboxdk/php-baseimages/php-cli:8.4-bookworm
environment:
LARAVEL_QUEUE: "true"
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE: "3"services:
worker:
image: ghcr.io/cboxdk/php-baseimages/php-cli:8.4-bookworm
environment:
LARAVEL_QUEUE: "true"
LARAVEL_QUEUE_HIGH: "true"
CBOX_INIT_PROCESS_QUEUE_DEFAULT_SCALE: "2"
CBOX_INIT_PROCESS_QUEUE_HIGH_SCALE: "2"// config/horizon.php
'environments' => [
'production' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['high', 'default', 'low'],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => 10,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
],
],
],services:
worker:
environment:
QUEUE_CONNECTION: redis
REDIS_HOST: redis
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
redis_data:services:
worker:
environment:
QUEUE_CONNECTION: database
DB_HOST: mysqlRun migration first:
php artisan queue:table
php artisan migrateservices:
worker:
environment:
QUEUE_CONNECTION: sqs
AWS_ACCESS_KEY_ID: your-key
AWS_SECRET_ACCESS_KEY: your-secret
SQS_PREFIX: https://sqs.us-east-1.amazonaws.com/your-account
SQS_QUEUE: your-queue// app/Jobs/ProcessOrder.php
class ProcessOrder implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $timeout = 120;
public $maxExceptions = 2;
public function __construct(
public Order $order
) {}
public function handle(): void
{
// Process order
}
public function failed(Throwable $exception): void
{
// Handle failure
}
}// Immediate dispatch
ProcessOrder::dispatch($order);
// Delayed dispatch
ProcessOrder::dispatch($order)->delay(now()->addMinutes(5));
// Specific queue
ProcessOrder::dispatch($order)->onQueue('high');
// Chain jobs
Bus::chain([
new ProcessOrder($order),
new SendConfirmation($order),
new NotifyAdmin($order),
])->dispatch();$batch = Bus::batch([
new ProcessOrder($order1),
new ProcessOrder($order2),
new ProcessOrder($order3),
])->then(function (Batch $batch) {
// All jobs completed
})->catch(function (Batch $batch, Throwable $e) {
// First failure
})->finally(function (Batch $batch) {
// Batch finished
})->dispatch();// routes/api.php
Route::get('/health/queue', function () {
$pending = Queue::size();
$failed = DB::table('failed_jobs')->count();
return response()->json([
'status' => $pending < 1000 ? 'healthy' : 'backlog',
'pending_jobs' => $pending,
'failed_jobs' => $failed,
]);
});// Access via Horizon dashboard or API
$metrics = app('horizon.metrics');
$throughput = $metrics->throughput();
$runtime = $metrics->runtime();# List failed jobs
php artisan queue:failed
# Retry all failed
php artisan queue:retry all
# Retry specific job
php artisan queue:retry 5
# Clear failed jobs
php artisan queue:flush// In job class
public function handle(): void
{
// Process in chunks to manage memory
Order::chunk(100, function ($orders) {
foreach ($orders as $order) {
$this->process($order);
}
});
}worker:
stop_grace_period: 30s
command: php artisan queue:work --timeout=25The worker will finish current job before shutting down.
class LongRunningJob implements ShouldQueue
{
public $timeout = 3600; // 1 hour
public function retryUntil(): DateTime
{
return now()->addHours(24);
}
}class ProcessPodcast implements ShouldQueue, ShouldBeUnique
{
public function uniqueId(): string
{
return $this->podcast->id;
}
public function uniqueFor(): int
{
return 60; // seconds
}
}# Scale workers
docker compose up -d --scale worker=5apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: queue-worker
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: queue-worker
minReplicas: 1
maxReplicas: 10
metrics:
- type: External
external:
metric:
name: redis_queue_size
target:
type: AverageValue
averageValue: 100- Check worker is running:
docker compose ps - Check queue connection:
php artisan queue:monitor - Check Redis connection:
redis-cli ping - Check job syntax:
php artisan queue:work --once
- Add
--max-jobs=1000to restart workers periodically - Process data in chunks
- Use
gc_collect_cycles()for complex jobs
- Increase
--timeoutvalue - Set job-specific
$timeoutproperty - Break into smaller jobs
- Use job chaining
- Check
failed_jobstable for errors - Implement proper error handling
- Use exponential backoff
- Add dead letter queue for inspection