Configuration
Configuration
Complete Queue Autoscale for Laravel configuration reference.
Table of Contents
- Prerequisites: Metrics Package Setup
- Basic Configuration
- Queue Configuration
- Worker Topology (v2)
- Strategy Configuration
- Policy Configuration
- Manager Configuration
- Advanced Options
- Environment Variables
- Configuration Patterns
Reading tip: the conceptual model for per-queue vs. group vs. exclusive vs. excluded workers lives in Queue Topology. This page is the reference for how to express each of those in config.
Prerequisites: Metrics Package Setup
Queue Autoscale for Laravel depends on laravel-queue-metrics for all queue discovery and metrics collection. The autoscaler cannot function without proper metrics configuration.
Quick Setup
# Install metrics package (if not already installed)
composer require cboxdk/laravel-queue-metrics
# Publish configuration
php artisan vendor:publish --tag=queue-metrics-config
# Configure storage backend in .env
QUEUE_METRICS_STORAGE=redis # Fast, in-memory (recommended)
# OR
QUEUE_METRICS_STORAGE=database # Persistent storage
Storage Configuration
Redis (Recommended for Production):
QUEUE_METRICS_STORAGE=redis
QUEUE_METRICS_CONNECTION=default
Ensure your Redis connection is configured in config/database.php.
Database (For Historical Persistence):
QUEUE_METRICS_STORAGE=database
Then publish and run migrations:
php artisan vendor:publish --tag=laravel-queue-metrics-migrations
php artisan migrate
📚 Full metrics package documentation: laravel-queue-metrics
Basic Configuration
Publish the config:
php artisan vendor:publish --tag=queue-autoscale-config
The defaults work out of the box. You only need to touch the config when you want to override the default profile, add per-queue overrides, declare groups/excluded queues, or tune global scaling parameters.
Minimal Configuration
<?php
use Cbox\LaravelQueueAutoscale\Configuration\Profiles\BalancedProfile;
return [
'enabled' => env('QUEUE_AUTOSCALE_ENABLED', true),
// Every queue discovered at runtime gets this profile unless overridden.
'sla_defaults' => BalancedProfile::class,
// Per-queue overrides. See "Queue Configuration" below.
'queues' => [],
];
Five profiles ship with the package (BalancedProfile, CriticalProfile, HighVolumeProfile, BurstyProfile, BackgroundProfile, plus the single-worker ExclusiveProfile). See Workload Profiles for what each one sets.
Queue Configuration
A queue entry takes one of two shapes:
Shape 1 — a profile class
use Cbox\LaravelQueueAutoscale\Configuration\Profiles\CriticalProfile;
'queues' => [
'payments' => CriticalProfile::class,
],
Pick the profile whose SLA + worker bounds match what you want. Nothing else required.
Shape 2 — a partial override array
When you want almost the defaults but with one or two changes, pass an array. It is deep-merged on top of sla_defaults:
'queues' => [
'exports' => [
'sla' => ['target_seconds' => 45],
'workers' => ['min' => 0, 'max' => 3],
],
],
The nested config shape
A fully-resolved queue configuration has four sections. You rarely need to see all of them — a profile populates them all — but here's the reference when you need to override specific keys:
'payments' => [
'sla' => [
'target_seconds' => 10, // pickup SLA; the most important single number
'percentile' => 99, // which percentile to measure against (50–99)
'window_seconds' => 120, // rolling window for the percentile
'min_samples' => 20, // below this many samples we fall back to oldest_job_age
],
'forecast' => [
'forecaster' => \Cbox\LaravelQueueAutoscale\Scaling\Calculators\LinearRegressionForecaster::class,
'policy' => \Cbox\LaravelQueueAutoscale\Scaling\Forecasting\Policies\AggressiveForecastPolicy::class,
'horizon_seconds' => 60,
'history_seconds' => 300,
],
'workers' => [
'min' => 5, // floor — autoscaler won't drop below this
'max' => 50, // ceiling — autoscaler won't exceed this
'tries' => 5, // --tries= on queue:work
'timeout_seconds' => 3600, // --max-time= on queue:work
'sleep_seconds' => 1, // --sleep= on queue:work
'shutdown_timeout_seconds' => 30,
'scalable' => true, // set false for pinned/exclusive queues
],
'spawn_compensation' => [
'enabled' => true,
'fallback_seconds' => 2.0,
'min_samples' => 3,
'ema_alpha' => 0.3,
],
],
The keys most operators touch:
sla.target_seconds— your SLA pickup target. Do not set below 5 seconds. The worker poll loop and job pickup overhead impose a hard floor of ~3-5s on pickup time, and targets below this will produce flaky breach events. Profiles withworkers.min = 0have an additional ~5-7s scale-from-zero overhead. See Understanding SLA Timing.workers.min/workers.max— floor and ceiling on concurrency.workers.scalable = false— pin the queue and bypass the scaling engine (see ExclusiveProfile).
Global scaling keys (cooldown, breach threshold, fallback job time) live under scaling.* at the top level — see the published config file.
Worker Topology (v2)
v2 introduces three new capabilities on top of per-queue autoscaling. Each is expressed as its own top-level config key. See Queue Topology for the conceptual explanation; this section is the config reference.
excluded — queues this package ignores
'excluded' => [
'horizon-managed', // exact match
'legacy-*', // fnmatch glob
'test-?', // fnmatch glob (single char)
],
- Patterns use PHP's
fnmatch()semantics. - An excluded queue is never discovered, evaluated, spawned, or terminated — even if the metrics package reports activity for it.
- The first time the manager sees an excluded queue in a cycle, it logs a single
infoline so you can confirm. - Exclusion wins over everything: if you put the same name in both
queuesandexcluded, it is excluded.
When to use: queues managed by Horizon or another supervisor, throwaway queues during migrations, or queues with workers started manually via queue:work under systemd/supervisord.
groups — multi-queue workers with strict priority
'groups' => [
'notifications' => [
'queues' => ['email', 'sms', 'push'], // priority order
'profile' => BalancedProfile::class, // optional — defaults to sla_defaults
'connection' => 'redis', // optional — defaults to 'default'
'mode' => 'priority', // only supported mode in v2
'overrides' => [ // optional partial override
'sla' => ['target_seconds' => 45],
],
],
],
- Each worker spawned for the group invokes
queue:work redis --queue=email,sms,push— Laravel polls them in that order per poll cycle. - The group is the scaling unit. Metrics are aggregated across members (
pending,throughput: summed;oldest_job_age: max). The SLA target is the group's SLA, not any individual queue's. - A queue may appear in at most one place: either under
queues.{name}or inside one group. Startup validation throwsInvalidConfigurationExceptionif this is violated. - Groups cannot use
ExclusiveProfile. A pinned group is a contradiction — use a per-queue exclusive config instead.
When to use: queues that share a failure domain and have compatible SLA expectations, where you want idle capacity in one queue to absorb bursts on another without paying spawn latency.
ExclusiveProfile — pinned single-worker queues
use Cbox\LaravelQueueAutoscale\Configuration\Profiles\ExclusiveProfile;
'queues' => [
'legacy-integration' => ExclusiveProfile::class,
],
workers.min = 1,workers.max = 1,workers.scalable = false.- The manager never evaluates scaling for this queue. Instead, it enforces exactly one live worker: respawns on death, terminates any duplicates.
- SLA breach events still fire for observability (operators need to know when a sequential queue falls behind) but scaling will not happen — the whole point is to preserve order.
- Jobs run strictly one at a time, in the order the queue driver delivers them.
When to use: third-party integrations that require single-connection semantics, customer workflows that assume jobs run in order, or any queue where two concurrent jobs would corrupt state.
Custom variation: a
PinnedProfilewithmin == max == Nandscalable: falsewould enforce "exactly N workers, always." TheWorkerConfigurationconstructor validates this invariant. We shipExclusiveProfile(N = 1) because it covers the most common case; write your own profile class if you need N > 1.
Strategy Configuration
Strategies determine HOW workers are calculated. The package includes a hybrid strategy by default.
Using Default Strategy
'strategy' => \Cbox\LaravelQueueAutoscale\Scaling\Strategies\HybridStrategy::class,
The hybrid strategy combines:
- Little's Law for steady-state
- Trend prediction for growing loads
- Backlog drain for SLA breaches
Custom Strategy
'strategy' => \App\Autoscale\Strategies\MyCustomStrategy::class,
See Custom Strategies for implementation guide.
Strategy Parameters
Some strategies accept additional configuration:
'strategy' => [
'class' => \Cbox\LaravelQueueAutoscale\Scaling\Strategies\HybridStrategy::class,
'options' => [
'trend_weight' => 0.7, // How much to trust trend predictions
'safety_margin' => 1.2, // 20% buffer for uncertainty
'min_trend_samples' => 3, // Samples needed for trend analysis
],
],
Policy Configuration
Policies add cross-cutting concerns (notifications, logging, etc.) to scaling operations.
Default Policies
The shipped default policies (set in the published config):
'policies' => [
\Cbox\LaravelQueueAutoscale\Policies\ConservativeScaleDownPolicy::class,
\Cbox\LaravelQueueAutoscale\Policies\BreachNotificationPolicy::class,
],
Available policy classes:
ConservativeScaleDownPolicy— limits scale-down to one worker per cycle (prevents thrashing)AggressiveScaleDownPolicy— allows rapid scale-down (for cost optimisation)NoScaleDownPolicy— never scales down (for strict capacity guarantees)BreachNotificationPolicy— logs SLA breach risks with built-in rate limiting (see Alerting)
Resource constraints and cooldown enforcement are built into the scaling engine itself, not expressed as policies — you don't configure them here.
Adding Custom Policies
'policies' => [
// Shipped defaults
\Cbox\LaravelQueueAutoscale\Policies\ConservativeScaleDownPolicy::class,
\Cbox\LaravelQueueAutoscale\Policies\BreachNotificationPolicy::class,
// Your own policies — any class implementing ScalingPolicy
\App\Autoscale\Policies\SlackNotificationPolicy::class,
\App\Autoscale\Policies\CostOptimizationPolicy::class,
],
Policy Order
Policies execute in the order listed. beforeScaling() hooks run top-to-bottom (each may modify the decision), then the scaling action fires, then afterScaling() hooks run top-to-bottom.
See Scaling Policies for implementation guide.
Manager Configuration
The AutoscaleManager orchestrates the entire autoscaling process.
Evaluation Interval
'evaluation_interval_seconds' => 30, // Check every 30 seconds
How often to evaluate scaling decisions:
- Lower values (10-30s): More responsive, higher resource usage
- Higher values (60-120s): Less responsive, lower resource usage
Balance based on:
- Queue traffic patterns
- SLA requirements
- System resources
Manager Options
'manager' => [
'evaluation_interval_seconds' => 30,
'max_concurrent_evaluations' => 5, // Parallel queue evaluations
'enable_metrics_collection' => true, // Collect performance data
'metrics_retention_hours' => 24, // How long to keep metrics
],
Advanced Options
Multiple queues with different SLAs
Pick the profile that matches each queue's SLA:
use Cbox\LaravelQueueAutoscale\Configuration\Profiles\BackgroundProfile;
use Cbox\LaravelQueueAutoscale\Configuration\Profiles\BalancedProfile;
use Cbox\LaravelQueueAutoscale\Configuration\Profiles\CriticalProfile;
'sla_defaults' => BalancedProfile::class,
'queues' => [
'critical' => CriticalProfile::class, // 10s SLA, 5-50 workers
'default' => BalancedProfile::class, // 30s SLA, 1-10 workers
'background' => BackgroundProfile::class, // 300s SLA, 0-5 workers
],
Per-queue overrides
When a profile is almost right but you want to adjust one or two values, pass an array. It deep-merges on top of sla_defaults:
'queues' => [
'exports' => [
'sla' => ['target_seconds' => 45],
'workers' => ['min' => 0, 'max' => 3],
],
],
Multiple queue connections
Queue names are keys into the queues map; the connection is resolved from your Laravel queue config. For one queue on a non-default connection, add a connection key:
'queues' => [
'notifications' => [
'connection' => 'sqs',
'sla' => ['target_seconds' => 30],
],
],
Resource limits (global)
Caps are under the top-level limits key:
'limits' => [
'max_cpu_percent' => 85, // Skip spawning when host CPU ≥ this
'max_memory_percent' => 85, // Skip spawning when host memory ≥ this
'worker_memory_mb_estimate' => 128, // Assumed memory footprint per worker
'reserve_cpu_cores' => 1, // Cores reserved for the OS/other services
],
These apply to every queue and group — they are how the package avoids spawning workers that would destabilise the host. See Resource Constraints for the math.
Business-hours scheduling
The config file is plain PHP, so any runtime logic is available:
use Cbox\LaravelQueueAutoscale\Configuration\Profiles\CriticalProfile;
use Cbox\LaravelQueueAutoscale\Configuration\Profiles\BackgroundProfile;
$isBusinessHours = now()->isWeekday() && now()->hour >= 9 && now()->hour < 17;
'queues' => [
'exports' => $isBusinessHours ? CriticalProfile::class : BackgroundProfile::class,
],
Gotcha: config is read once per manager start. Business-hours swaps require you to schedule a manager restart when the window changes (e.g. at 09:00 and 17:00).
Environment Variables
A small set of environment variables is wired into the shipped config file. Anything else is a plain PHP key — change the file instead.
# Enable/disable the manager
QUEUE_AUTOSCALE_ENABLED=true
# Optional explicit manager/node ID override.
# Leave unset to use the built-in auto-generated node identity.
QUEUE_AUTOSCALE_MANAGER_ID=web-01
# Optional signal backends.
# auto => null/no-op on single host, Redis-backed in cluster mode
# redis => force Redis-backed signal storage
# null => force fallback/no-op signal storage
QUEUE_AUTOSCALE_PICKUP_TIME_STORE=auto
QUEUE_AUTOSCALE_SPAWN_LATENCY_TRACKER=auto
# Enable only when multiple managers run against the same queues
QUEUE_AUTOSCALE_CLUSTER_ENABLED=false
# Fallback job time when metrics aren't available yet (seconds)
QUEUE_AUTOSCALE_FALLBACK_JOB_TIME=2.0
# Alert cooldown for BreachNotificationPolicy / AlertRateLimiter (seconds)
QUEUE_AUTOSCALE_ALERT_COOLDOWN=300
# Log channel the manager writes to
QUEUE_AUTOSCALE_LOG_CHANNEL=stack
Per-queue SLA targets are not env-driven — they live in profile classes or queue-level override arrays. If you need per-queue env configuration, author a custom Profile class that reads env inside resolve().
Signal backend modes
QUEUE_AUTOSCALE_PICKUP_TIME_STORE=autokeeps single-host mode Redis-free and switches to Redis automatically in cluster mode.QUEUE_AUTOSCALE_SPAWN_LATENCY_TRACKER=autofollows the same rule for spawn-latency compensation.- Set either key to
redisif you want Redis-backed predictive signals on a single host. - Set either key to
nullif you want to force fallback behaviour even when Redis exists.
Configuration Patterns
Conservative — stability over responsiveness
Use BalancedProfile with a wider cooldown:
'sla_defaults' => BalancedProfile::class,
'scaling' => ['cooldown_seconds' => 120],
Aggressive — fast reactions to bursts
Use CriticalProfile (10s SLA, p99, short cooldown). Nothing else to tune — the profile's forecast policy is already aggressive.
Cost-optimised — can scale to zero
Use BackgroundProfile (min=0, max=5) for queues that can tolerate multi-minute SLA:
'queues' => [
'cleanup' => BackgroundProfile::class,
],
Multi-tier
Pick a profile per tier:
'queues' => [
'tier-1-realtime' => CriticalProfile::class,
'tier-2-user-facing' => HighVolumeProfile::class,
'tier-3-standard' => BalancedProfile::class,
'tier-4-background' => BackgroundProfile::class,
],
Configuration Validation
Config validation runs at manager startup. If anything is wrong, php artisan queue:autoscale fails with a specific InvalidConfigurationException pointing at the offending key. The common ones:
workers.min must be >= 0/workers.max (X) must be >= workers.min (Y)— inconsistent worker bounds.workers.scalable=false requires workers.min (X) to equal workers.max (Y)— non-scalable (pinned) configs must declare exactly one target count.workers.scalable=false requires workers.min >= 1— a pinned queue needs at least one worker.Group 'X' cannot use a non-scalable profile— you pointed a group atExclusiveProfile; use a per-queue exclusive config instead.Queue 'X' is configured both in 'queues' and in group 'Y'— each queue may only appear once acrossqueuesand all groups.Queue 'X' appears in multiple groups (...)— a queue may only belong to one group.Group 'X' must declare at least one queue— the group'squeueslist was empty.
Fix the config and restart the manager.
Configuration Testing
There's no separate dry-run command — the manager evaluates on a fixed interval. To test a config change without a deploy:
# Run the manager in very-verbose mode. It prints every decision with
# reasoning, but only spawns/terminates when the decision differs from
# the current worker count.
php artisan queue:autoscale -vvv --interval=5
# In another terminal, push some representative work onto the target
# queue. Anything your app already dispatches works — for a quick smoke
# test, queued closures via tinker:
php artisan tinker
>>> for ($i = 0; $i < 50; $i++) { dispatch(function () { sleep(1); })->onQueue('critical'); }
Watch the manager output. If the decisions surprise you, inspect the debug state directly:
php artisan queue:autoscale:debug --queue=critical --connection=redis
See Also
- How It Works - Understanding the scaling algorithm
- Custom Strategies - Writing custom scaling strategies
- Scaling Policies - Implementing scaling policies
- Deployment - Production deployment guide
- Monitoring - Monitoring and observability