PHPackages                             cboxdk/laravel-queue-autoscale - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Queues &amp; Workers](/categories/queues)
4. /
5. cboxdk/laravel-queue-autoscale

ActiveLibrary[Queues &amp; Workers](/categories/queues)

cboxdk/laravel-queue-autoscale
==============================

Intelligent, predictive autoscaling for Laravel queues with SLA/SLO-based optimization

v4.0.0(2w ago)102.8kMITPHPPHP ^8.4|^8.5CI passing

Since Jan 21Pushed 2w agoCompare

[ Source](https://github.com/cboxdk/laravel-queue-autoscale)[ Packagist](https://packagist.org/packages/cboxdk/laravel-queue-autoscale)[ Docs](https://github.com/cboxdk/laravel-queue-autoscale)[ GitHub Sponsors](https://github.com/cboxdk)[ RSS](/packages/cboxdk-laravel-queue-autoscale/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (10)Dependencies (86)Versions (49)Used By (0)

Queue Autoscale for Laravel
===========================

[](#queue-autoscale-for-laravel)

[![Latest Version on Packagist](https://camo.githubusercontent.com/b4f4610ff91098436d03284bd905bf8798d0de6bfe4362dc25e95bf21cce555a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f63626f78646b2f6c61726176656c2d71756575652d6175746f7363616c652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/cboxdk/laravel-queue-autoscale)[![GitHub Tests Action Status](https://camo.githubusercontent.com/74c9b2d1786facfd3a6f37d6a392412ea909e9af791a353420fdbeccb525c769/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f63626f78646b2f6c61726176656c2d71756575652d6175746f7363616c652f74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/cboxdk/laravel-queue-autoscale/actions?query=workflow%3Atests+branch%3Amain)[![GitHub Code Quality Action Status](https://camo.githubusercontent.com/f7cfa416c304c3a2f24503340c7d3c2b0dc38388e2315c1f0fdb6e292ca0787c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f63626f78646b2f6c61726176656c2d71756575652d6175746f7363616c652f636f64652d7175616c6974792e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307175616c697479267374796c653d666c61742d737175617265)](https://github.com/cboxdk/laravel-queue-autoscale/actions?query=workflow%3Acode-quality+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/8b98c90fd24c0b8e87f5c3cfbe20dda86a7a61e908bf22c3e987207ca4985eba/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f63626f78646b2f6c61726176656c2d71756575652d6175746f7363616c652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/cboxdk/laravel-queue-autoscale)

**SLA-driven autoscaling for Laravel queue workers.**

Queue Autoscale for Laravel is a long-running worker manager that spawns and terminates `queue:work`processes to hold a pickup-time SLA. Instead of configuring worker counts, you declare how long a job may wait before it is picked up, and the manager solves for the worker count each evaluation cycle using queueing theory (Little's Law) and backlog-drain math, bounded by measured CPU and memory capacity on the host.

Features
--------

[](#features)

- **SLA-based scaling** — declare a target pickup time; worker counts are derived, not configured
- **Little's Law steady state** — `workers = arrival rate × average job time` for the baseline
- **Backlog drain with progressive urgency** — a quadratic aggressiveness curve that ramps from 1.0x at half the SLA budget to 3.0x at the SLA target, capped at 5.0x
- **p95 pickup-time signal** — sliding-window percentile over real observed pickup times, with a fallback to oldest-job age when there are not enough samples
- **Spawn-latency compensation** — measured spawn time (EMA) is subtracted from the SLA budget
- **Failure fuse** — a downstream outage looks like load to any autoscaler; the fuse detects a high failure rate, holds the queue at `workers.min`, then probes with a single worker before releasing
- **Resource-aware** — CPU and memory ceilings measured on the host constrain every decision
- **Metrics-driven** — queue discovery and metrics come from [`laravel-queue-metrics`](https://github.com/cboxdk/laravel-queue-metrics)
- **Cluster-aware** — managers auto-join via Redis, elect a leader, and distribute worker targets across hosts
- **Worker groups** — one worker set polling several queues in strict priority order
- **Queues matched by pattern** — `scrape-tenant-*` governs every tenant queue, so runtime-generated names need no configuration entry of their own
- **Configuration check** — `queue:autoscale:doctor` reports configurations that are valid and still govern the wrong queues
- **Testable** — fakes and assertions in `src/Testing` for proving what your own configuration does
- **Extensible** — custom scaling strategies and policies via interfaces
- **Events** — react to scaling decisions, SLA breaches, fuse transitions and cluster changes
- **Graceful shutdown** — SIGTERM, then SIGKILL after the shutdown timeout

Requirements
------------

[](#requirements)

- PHP 8.4 or 8.5
- Laravel 12 or 13
- `ext-pcntl` and `ext-posix` (the manager is a signal-handling daemon)
- [`cboxdk/laravel-queue-metrics`](https://github.com/cboxdk/laravel-queue-metrics) `^3.0`

Redis is required only for cluster mode. Single-host mode works with any queue driver and needs no Redis. [`cboxdk/laravel-telemetry`](https://github.com/cboxdk/laravel-telemetry) is optional and enables the OpenTelemetry integration; it requires Laravel 12+.

Installation
------------

[](#installation)

```
composer require cboxdk/laravel-queue-autoscale
```

Run the interactive installer to publish config, choose a topology, and generate matching `.env`values:

```
php artisan queue:autoscale:install
```

It offers three presets via `--topology=`:

PresetShape`single-low`single host, low traffic, no Redis infrastructure`single-redis`single host with Redis-backed metrics and predictive signals`cluster`multi-host cluster with Redis coordinationAdditional flags: `--metrics-connection=`, `--publish-migrations`, `--write-env`, `--env-file=`, `--force`, `--no-publish`.

If you prefer the manual path, publish the config yourself:

```
php artisan vendor:publish --tag=queue-autoscale-config
```

### Set up the metrics package

[](#set-up-the-metrics-package)

This package does not discover queues or collect metrics itself. Both come from [`laravel-queue-metrics`](https://github.com/cboxdk/laravel-queue-metrics), which is installed as a dependency:

```
php artisan vendor:publish --tag=queue-metrics-config
```

Configure its storage backend in `.env`:

```
# Option A: Redis (fast, in-memory)
QUEUE_METRICS_STORAGE=redis
QUEUE_METRICS_CONNECTION=default

# Option B: Database (persistent)
QUEUE_METRICS_STORAGE=database
```

If you use database storage, publish and run its migrations:

```
php artisan vendor:publish --tag=queue-metrics-migrations
php artisan migrate
```

### Choose an autoscale topology

[](#choose-an-autoscale-topology)

```
# Single host without Redis
QUEUE_AUTOSCALE_CLUSTER_ENABLED=false
QUEUE_AUTOSCALE_PICKUP_TIME_STORE=auto
QUEUE_AUTOSCALE_SPAWN_LATENCY_TRACKER=auto

# Single host with Redis-backed predictive signals
QUEUE_AUTOSCALE_PICKUP_TIME_STORE=redis
QUEUE_AUTOSCALE_SPAWN_LATENCY_TRACKER=redis

# Multi-host cluster
QUEUE_AUTOSCALE_CLUSTER_ENABLED=true
```

`auto` keeps single-host mode Redis-free and switches to Redis-backed coordination in cluster mode. The installer can write these values into `.env` for you with `--write-env`.

Quick Start
-----------

[](#quick-start)

### 1. Configure SLA targets (optional)

[](#1-configure-sla-targets-optional)

By default `sla_defaults` is `BalancedProfile::class` — a 30-second p95 pickup-time target with 1–10 workers. If that suits you, there is nothing to configure.

To customise, edit `config/queue-autoscale.php`. Each entry is **either** a `ProfileContract` class **or** a literal array that is deep-merged over `sla_defaults`:

```
use Cbox\LaravelQueueAutoscale\Configuration\Profiles\BalancedProfile;
use Cbox\LaravelQueueAutoscale\Configuration\Profiles\ConnectionLimitedProfile;
use Cbox\LaravelQueueAutoscale\Configuration\Profiles\CriticalProfile;

return [
    'enabled' => true,

    // Shipped profiles: BalancedProfile, CriticalProfile, HighVolumeProfile,
    // BurstyProfile, BackgroundProfile, ExclusiveProfile, ConnectionLimitedProfile.
    'sla_defaults' => BalancedProfile::class,

    'queues' => [
        // A profile class...
        'payments' => CriticalProfile::class,

        // ...or a partial override merged over sla_defaults.
        'reports' => [
            'sla' => ['target_seconds' => 120],
            'workers' => ['min' => 0, 'max' => 4],
        ],

        // ...or a glob, for queue names generated at runtime. An exact name
        // above always wins over a pattern.
        'scrape-tenant-*' => [
            'profile' => ConnectionLimitedProfile::class,
            'workers' => ['max' => 5],
        ],
    ],
];
```

A per-queue entry does **not** accept `'profile'` and `'overrides'` keys — those belong to `groups`only. See [Workload Profiles](docs/basic-usage/workload-profiles.md).

### 2. Run the autoscaler

[](#2-run-the-autoscaler)

```
php artisan queue:autoscale

# Custom evaluation interval (default: 5 seconds)
php artisan queue:autoscale --interval=10

# Stop the existing local manager and take over its host lock
php artisan queue:autoscale --replace

# Inspect cluster leader, hosts, capacity and workload targets
php artisan queue:autoscale:cluster

# Inspect one queue's raw state, metrics and fuse status
php artisan queue:autoscale:debug --queue=payments

# Check the configuration against the queues that actually exist
php artisan queue:autoscale:doctor

# Signal running managers to restart gracefully (use this on deploy)
php artisan queue:autoscale:restart
```

Each cycle the manager:

1. Pulls queue metrics from `laravel-queue-metrics`
2. Runs the configured strategy to get a target worker count
3. Constrains that target by measured host CPU/memory capacity
4. Clamps it to `workers.min` / `workers.max`
5. Applies the failure fuse
6. Runs the registered policies over the resulting decision
7. Spawns or terminates workers, and dispatches events

### 3. Monitor with events

[](#3-monitor-with-events)

```
use Cbox\LaravelQueueAutoscale\Events\SlaBreachPredicted;
use Cbox\LaravelQueueAutoscale\Events\WorkersScaled;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;

Event::listen(WorkersScaled::class, function (WorkersScaled $event): void {
    Log::info("Scaled {$event->queue}: {$event->from} → {$event->to} workers");
    Log::info("Reason: {$event->reason}");
});

Event::listen(SlaBreachPredicted::class, function (SlaBreachPredicted $event): void {
    $decision = $event->decision;

    Log::warning("SLA breach predicted for {$decision->queue}", [
        'predicted_pickup' => $decision->predictedPickupTime,
        'sla_target' => $decision->slaTarget,
    ]);
});
```

How It Works
------------

[](#how-it-works)

### The hybrid strategy

[](#the-hybrid-strategy)

`HybridStrategy` (the default) computes two candidate worker counts and takes the **maximum**:

**1. Steady state — Little's Law**

```
workers = arrivalRate × avgJobTime

```

The arrival rate comes from `ArrivalRateEstimator`, which tracks backlog deltas and blends in a forecast. It is used only when its confidence clears `scaling.min_arrival_rate_confidence` (0.5); otherwise the observed processing rate (`throughputPerMinute / 60`) is used instead. When the failure rate exceeds 5%, an estimated retry volume is subtracted so that retries are not counted as new arrivals.

**2. Backlog drain — SLA protection**

```
slaProgress   = min(slaSignal / effectiveSla, 1.5)
baseWorkers   = backlog / max((effectiveSla - slaSignal) / avgJobTime, 1.0)
multiplier    = min(1.0 + 8.0 × (slaProgress - 0.5)², 5.0)
workers       = baseWorkers × multiplier

```

This returns zero until `slaProgress` reaches `scaling.breach_threshold` (default `0.5`, i.e. half the SLA budget consumed). The multiplier then ramps continuously: 1.0x at 50%, ~1.72x at 80%, 3.0x at 100%, capped at 5.0x. `effectiveSla` is the SLA target minus the measured spawn latency, and `slaSignal` is the p95 of recent observed pickup times, falling back to oldest-job age when there are too few samples.

The larger of the two wins. A saturation guard then bumps the target to `activeWorkers + 1` if workers are above 90% utilisation but neither calculation asked for more. Finally the target is clamped to `[workers.min, workers.max]` and passed through a hysteresis smoother that limits scale-down to one worker per cycle while throughput is stable.

### Constraints applied to every decision

[](#constraints-applied-to-every-decision)

- **Host capacity** — CPU and memory ceilings from measured system metrics, expressed as `currentWorkers + additional headroom` and reduced by what other queues on the host already use
- **Config bounds** — `workers.min` and `workers.max` from the queue's profile
- **Failure fuse** — holds a queue at `workers.min` while its failure rate is above threshold
- **Anti-flapping cooldown** — `scaling.cooldown_seconds` (default 60) blocks only a *reversal* of scaling direction; scaling further in the same direction is always allowed, and an active SLA breach overrides the cooldown for scale-up

See the [Architecture](docs/algorithms/architecture.md) deep dive for the full derivation.

Configuration Reference
-----------------------

[](#configuration-reference)

The published `config/queue-autoscale.php` is documented inline. The keys most people touch:

```
'sla_defaults' => BalancedProfile::class,   // ProfileContract class or literal array
'queues' => [],                             // per-queue: profile class or partial override array
'excluded' => [],                           // fnmatch globs never managed, e.g. 'legacy-*'
'groups' => [],                             // multi-queue workers with strict priority

'scaling' => [
    'fallback_job_time_seconds' => 2.0,     // used when metrics have no avg duration
    'breach_threshold' => 0.5,              // fraction of SLA budget before backlog drain engages
    'cooldown_seconds' => 60,               // anti-flapping window for direction reversals
],

'limits' => [
    'max_cpu_percent' => 85,
    'max_memory_percent' => 85,
    'worker_memory_mb_estimate' => 128,     // cold-start estimate; measured data wins once available
    'worker_cpu_core_estimate' => 0.2,
    'reserve_cpu_cores' => 0.2,
],

'manager' => [
    // Fallback drain window for workers whose queue config no longer resolves.
    'shutdown_grace_seconds' => 30,
],

'strategy' => HybridStrategy::class,        // a plain class string, not an array

'policies' => [
    ConservativeScaleDownPolicy::class,
    BreachNotificationPolicy::class,
],
```

A per-queue entry may also carry a `resources` block declaring cold-start CPU/memory estimates for that queue's workers:

```
'queues' => [
    'video-encode' => [
        'workers' => ['min' => 0, 'max' => 4],
        'resources' => ['cpu_cores' => 1.0, 'memory_mb' => 2048],
    ],
],
```

`manager.evaluation_interval_seconds` (default 5) sets the evaluation interval; `queue:autoscale --interval=` overrides it for a single process.

Every key, including the profile, forecast, fuse, pickup-time, spawn-latency, cluster, alerting and telemetry blocks, is covered in [Configuration](docs/basic-usage/configuration.md).

Custom Scaling Strategies
-------------------------

[](#custom-scaling-strategies)

A strategy answers one question: how many workers should this queue have right now?

```
use Cbox\LaravelQueueAutoscale\Configuration\QueueConfiguration;
use Cbox\LaravelQueueAutoscale\Contracts\ScalingStrategyContract;
use Cbox\LaravelQueueMetrics\DataTransferObjects\QueueMetricsData;

final class CustomStrategy implements ScalingStrategyContract
{
    private int $lastTarget = 0;

    public function calculateTargetWorkers(QueueMetricsData $metrics, QueueConfiguration $config): int
    {
        // avgDuration is already in seconds by the time a strategy sees it.
        $jobsPerSecond = $metrics->throughputPerMinute / 60.0;
        $target = (int) ceil($jobsPerSecond * max($metrics->avgDuration, 0.1) * 2);

        return $this->lastTarget = max(
            $config->workers->min,
            min($config->workers->max, $target),
        );
    }

    public function getLastReason(): string
    {
        return "Custom strategy: doubled steady-state demand → {$this->lastTarget} workers";
    }

    public function getLastPrediction(): ?float
    {
        return null; // Optional: predicted pickup time in seconds
    }
}
```

Register it as a plain class string:

```
'strategy' => \App\Scaling\CustomStrategy::class,
```

The engine still applies capacity limits, config bounds and the fuse on top of whatever a strategy returns. See [Custom Strategies](docs/advanced-usage/custom-strategies.md).

Scaling Policies
----------------

[](#scaling-policies)

A policy runs *after* the strategy and engine have produced a `ScalingDecision`. `beforeScaling()`may return a modified decision (or `null` to leave it alone); `afterScaling()` observes the result.

```
use Cbox\LaravelQueueAutoscale\Contracts\ScalingPolicy;
use Cbox\LaravelQueueAutoscale\Scaling\ScalingDecision;

final class BusinessHoursFloorPolicy implements ScalingPolicy
{
    public function beforeScaling(ScalingDecision $decision): ?ScalingDecision
    {
        if (! $decision->shouldScaleDown() || ! now()->isWeekday()) {
            return null;
        }

        if ($decision->targetWorkers >= 2) {
            return null;
        }

        return new ScalingDecision(
            connection: $decision->connection,
            queue: $decision->queue,
            currentWorkers: $decision->currentWorkers,
            targetWorkers: 2,
            reason: "business-hours floor of 2 applied (was: {$decision->reason})",
            predictedPickupTime: $decision->predictedPickupTime,
            slaTarget: $decision->slaTarget,
        );
    }

    public function afterScaling(ScalingDecision $decision): void
    {
        //
    }
}
```

Register **class strings** — the loader resolves each through the container, so constructor injection works. An instance or closure placed in this array is silently ignored:

```
'policies' => [
    \App\Policies\BusinessHoursFloorPolicy::class,
],
```

Policies are chained: a non-null return becomes the decision the next policy sees. An exception thrown by a policy is caught and logged, and scaling continues. See [Scaling Policies](docs/basic-usage/scaling-policies.md).

Events
------

[](#events)

All events live in `Cbox\LaravelQueueAutoscale\Events`.

EventProperties`ScalingDecisionMade``decision``SlaBreachPredicted``decision``WorkersScaled``connection`, `queue`, `from`, `to`, `action`, `reason``SlaBreached``connection`, `queue`, `oldestJobAge`, `slaTarget`, `pending`, `activeWorkers``SlaRecovered``connection`, `queue`, `currentJobAge`, `slaTarget`, `pending`, `activeWorkers``FuseTripped``connection`, `queue`, `failureRate`, `samples`, `failures`, `thresholdPercent`, `heldAtWorkers``FuseProbing``connection`, `queue`, `probeWorkers`, `cooldownSeconds``FuseRecovered``connection`, `queue`, `failureRate`, `samples``AutoscaleManagerStarted``managerId`, `host`, `clusterEnabled`, `clusterId`, `intervalSeconds`, `startedAt`, `packageVersion``AutoscaleManagerStopped``managerId`, `host`, `clusterEnabled`, `clusterId`, `startedAt`, `stoppedAt`, `reason`, `workerCount`, `packageVersion``ClusterLeaderChanged``clusterId`, `previousLeaderId`, `currentLeaderId`, `observedByManagerId`, `changedAt``ClusterManagerPresenceChanged``clusterId`, `managerIds`, `addedManagerIds`, `removedManagerIds`, `leaderId`, `observedByManagerId`, `observedAt``ClusterSummaryPublished``clusterId`, `leaderId`, `summary`, `publishedAt````
use Cbox\LaravelQueueAutoscale\Events\ScalingDecisionMade;
use Cbox\LaravelQueueAutoscale\Events\WorkersScaled;

Event::listen(ScalingDecisionMade::class, function (ScalingDecisionMade $event): void {
    $decision = $event->decision;

    Log::info('Scaling decision', [
        'queue' => $decision->queue,
        'current' => $decision->currentWorkers,
        'target' => $decision->targetWorkers,
        'action' => $decision->action(),   // 'scale_up' | 'scale_down' | 'hold'
        'reason' => $decision->reason,
    ]);
});

Event::listen(WorkersScaled::class, function (WorkersScaled $event): void {
    Metrics::gauge('queue.workers', $event->to, [
        'queue' => $event->queue,
        'action' => $event->action,        // 'up' | 'down'
    ]);
});
```

`WorkersScaled::$action` is the literal string `'up'` or `'down'`. `ScalingDecision::action()` uses a different vocabulary (`'scale_up'`, `'scale_down'`, `'hold'`) — do not mix them up. There is no `confidence` property on `ScalingDecision`, and no worker-health event.

See [Event Handling](docs/basic-usage/event-handling.md).

Running as a Daemon
-------------------

[](#running-as-a-daemon)

The manager is a long-running process. Run exactly one per app in single-host mode, and exactly one per host in cluster mode. Use Supervisor to keep it alive:

```
[program:queue-autoscale]
command=php /path/to/artisan queue:autoscale --interval=5
directory=/path/to/project
user=www-data
autostart=true
autorestart=true
stopsignal=TERM
redirect_stderr=true
stdout_logfile=/path/to/logs/autoscale.log
```

On deploy, restart it through Artisan so it drains its workers before Supervisor starts the new release:

```
php artisan queue:autoscale:restart
```

With `manager.honor_queue_restart` enabled (the default), a plain `php artisan queue:restart` also stops the manager gracefully, so a standard deploy pipeline needs no extra step.

See [Deployment](docs/deployment/_index.md) for Forge, Ploi, Docker and self-hosted recipes.

Metrics Integration
-------------------

[](#metrics-integration)

**This package does not discover queues or collect metrics itself.** Both come from [`laravel-queue-metrics`](https://github.com/cboxdk/laravel-queue-metrics):

```
use Cbox\LaravelQueueMetrics\Facades\QueueMetrics;

$metrics = QueueMetrics::getQueueMetrics('redis', 'payments');

echo "Queue: {$metrics->connection}/{$metrics->queue}\n";
echo "Pending: {$metrics->pending} jobs\n";
echo "Oldest job age: {$metrics->oldestJobAge}s\n";
echo "Throughput: {$metrics->throughputPerMinute} jobs/min\n";
echo "Avg duration: {$metrics->avgDuration}\n";
echo "Active workers: {$metrics->activeWorkers}\n";
```

`QueueMetrics::getAllQueuesWithMetrics()` returns a keyed array of raw metric arrays; use `getQueueMetrics()` when you want the `QueueMetricsData` object.

**Division of responsibility**

[laravel-queue-metrics](https://github.com/cboxdk/laravel-queue-metrics)laravel-queue-autoscaleScans configured queue connectionsApplies the scaling algorithmsDiscovers active queuesMakes SLA-based scaling decisionsCollects depth, age and duration metricsManages the worker pool lifecycleCalculates throughput and failure ratesEnforces CPU/memory constraintsTracks worker heartbeatsRuns policies and dispatches eventsOpenTelemetry via laravel-telemetry
-----------------------------------

[](#opentelemetry-via-laravel-telemetry)

When [`cboxdk/laravel-telemetry`](https://github.com/cboxdk/laravel-telemetry) is installed, the autoscaler publishes its scaling signals automatically — no configuration needed. Disable with `QUEUE_AUTOSCALE_TELEMETRY_ENABLED=false`.

When it is not installed, `queue:autoscale:debug` reports `Telemetry: not installed` and everything else carries on unchanged — the integration is optional, not a dependency.

MetricTypeUnitLabels`queue_autoscale.workers.target`gauge`{workers}``connection`, `queue``queue_autoscale.sla.predicted_pickup`gauge`s``connection`, `queue``queue_autoscale.sla.target`gauge`s``connection`, `queue``queue_autoscale.sla.breach`gauge`1``connection`, `queue``queue_autoscale.capacity.max_workers`gauge`{workers}``limiter``queue_autoscale.fuse.state`gauge`1``connection`, `queue``queue_autoscale.scaling.actions`counter`{actions}``connection`, `queue`, `direction``queue_autoscale.sla.breaches`counter`{breaches}``connection`, `queue``queue_autoscale.fuse.trips`counter`{trips}``connection`, `queue``queue_autoscale.cluster.leader_changes`counter`{changes}`—`queue_autoscale.cluster.managers`gauge (observable)`{managers}`—`queue_autoscale.cluster.workers`gauge (observable)`{workers}`—`queue_autoscale.cluster.required_workers`gauge (observable)`{workers}`—`queue_autoscale.cluster.worker_capacity`gauge (observable)`{workers}`—`queue_autoscale.cluster.utilization`gauge (observable)`%`—`queue_autoscale.cluster.recommended_hosts`gauge (observable)`{hosts}`—`queue_autoscale.cluster.host_workers`gauge (observable)`{workers}``host``queue_autoscale.cluster.host_capacity`gauge (observable)`{workers}``host`Scaling actions, SLA breaches and recoveries, fuse transitions, manager start/stop and cluster leader changes are also emitted as structured OTLP events (`queue_autoscale.scaling.action`, `queue_autoscale.sla.breached`, `queue_autoscale.fuse.tripped`, …) carrying the full context — including the scaling `reason`, which is deliberately not a metric label.

Deliberately **not** exported: queue depth, oldest-job age, health scores, worker busy/idle state and job baselines (owned by `cboxdk/laravel-queue-metrics`), and per-job durations/outcomes (covered by laravel-telemetry's own queue instrumentation). There is no active-worker gauge here — queue-metrics' `queue_metrics.queue.active_workers` gauge is the one to join against `queue_autoscale.workers.target` in your dashboards.

Metrics are shipped to your OTLP endpoint by the telemetry package's `telemetry:flush` (cron or `--daemon`) — make sure one is scheduled.

Testing
-------

[](#testing)

### Testing your own configuration

[](#testing-your-own-configuration)

The package ships fakes and assertions so an application can prove what its queues will do, without Redis and without waiting for load:

```
use Cbox\LaravelQueueAutoscale\Testing\InteractsWithAutoscaling;
use Cbox\LaravelQueueAutoscale\Testing\QueueMetricsFactory;

uses(InteractsWithAutoscaling::class);

test('no tenant ever gets a sixth connection', function () {
    $this->assertWorkersCappedAt(5, 'scrape-tenant-42');
});

test('a failing provider stops the queue scaling up', function () {
    $behind = QueueMetricsFactory::behind(1000, oldestJobAge: 300, queue: 'payments');

    $this->tripFuseFor('payments');

    expect($this->workersDemandedFor($behind))->toBe(0);
});
```

See [Testing Your Configuration](docs/basic-usage/testing.md).

### Testing the package itself

[](#testing-the-package-itself)

```
composer test              # run the suite
composer test-coverage     # run with coverage
composer analyse           # PHPStan / Larastan
vendor/bin/pint            # code style
```

SQS and FIFO specs run against [ElasticMQ](https://github.com/softwaremill/elasticmq) and skip when it is not running:

```
docker run -d --name autoscale-elasticmq -p 9324:9324 softwaremill/elasticmq-native:1.6.11
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for recent changes.

Contributing
------------

[](#contributing)

Please see the [Contributing Guide](docs/advanced-usage/contributing.md) for details.

Security
--------

[](#security)

Please report security issues privately through [GitHub Private Vulnerability Reporting](https://github.com/cboxdk/laravel-queue-autoscale/security/advisories/new)rather than the public issue tracker. This is a community-maintained package; reports are handled on a best-effort basis, and fixes land on the current major line. See [Security](docs/advanced-usage/security.md).

Credits
-------

[](#credits)

- [Sylvester Damgaard](https://github.com/sylvesterdamgaard)
- [All Contributors](../../contributors)

Resources
---------

[](#resources)

### Documentation

[](#documentation)

- **[Introduction](docs/index.md)** — what the package is and when to reach for it
- **[Quick Start](docs/quickstart.md)** — one queue autoscaled in five minutes
- **[Installation](docs/basic-usage/installation.md)** — full install and configuration walkthrough
- **[Architecture](docs/algorithms/architecture.md)** — deep dive into the algorithms and system design
- **[Troubleshooting](docs/basic-usage/troubleshooting.md)** — common issues and debugging
- **[examples/README.md](examples/README.md)** — templates for custom strategies and policies

### Examples

[](#examples)

- **Custom strategies**
    - [TimeBasedStrategy](examples/Strategies/TimeBasedStrategy.php) — scale on time-of-day patterns
    - [CostOptimizedStrategy](examples/Strategies/CostOptimizedStrategy.php) — conservative scaling
- **Custom policies**
    - [SlackNotificationPolicy](examples/Policies/SlackNotificationPolicy.php) — Slack alerts on scaling events
    - [MetricsLoggingPolicy](examples/Policies/MetricsLoggingPolicy.php) — log detailed metrics to a dedicated file

> `examples/config-examples.php` is written against the current schema. The strategy and policy classes implement the real contracts and are meant to be adapted, not dropped in as-is. The authoritative reference is [the documentation](docs/index.md).

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

54

—

FairBetter than 97% of packages

Maintenance98

Actively maintained with recent releases

Popularity29

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity66

Established project with proven stability

 Bus Factor1

Top contributor holds 94.9% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~8 days

Total

24

Last Release

14d ago

Major Versions

v2.1.0 → v3.0.02026-04-26

v3.11.1 → v4.0.02026-08-05

PHP version history (2 changes)v2.0.0PHP ^8.3|^8.4|^8.5

v4.0.0PHP ^8.4|^8.5

### Community

Maintainers

![](https://www.gravatar.com/avatar/b9761a79e61f2d5b9d650510dfb3555da18daf38f027aa84012c937e397e39a7?d=identicon)[cboxdk](/maintainers/cboxdk)

---

Top Contributors

[![sylvesterdamgaard](https://avatars.githubusercontent.com/u/2431914?v=4)](https://github.com/sylvesterdamgaard "sylvesterdamgaard (261 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (11 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (3 commits)")

---

Tags

autoscaleautoscalingcboxlaravellittles-lawpredictivequeueslasloworkerlaravelqueueworkerslaslocboxautoscalingpredictiveautoscalelittles-law

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/cboxdk-laravel-queue-autoscale/health.svg)

```
[![Health](https://phpackages.com/badges/cboxdk-laravel-queue-autoscale/health.svg)](https://phpackages.com/packages/cboxdk-laravel-queue-autoscale)
```

###  Alternatives

[laravel/horizon

Dashboard and code-driven configuration for Laravel queues.

4.2k99.8M355](/packages/laravel-horizon)[spatie/laravel-health

Monitor the health of a Laravel application

88212.7M189](/packages/spatie-laravel-health)[harris21/laravel-fuse

Circuit breaker for Laravel queue jobs. Protect your workers from cascading failures.

46273.9k](/packages/harris21-laravel-fuse)[croustibat/filament-jobs-monitor

Background Jobs monitoring like Horizon for all drivers for FilamentPHP

278359.3k12](/packages/croustibat-filament-jobs-monitor)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3915.5k](/packages/rawilk-profile-filament-plugin)[finller/laravel-media

A flexible media library for Laravel

472.1k](/packages/finller-laravel-media)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
