robuust / laravel-heroku-queue-listener
Laravel package to autoscale Heroku queue workers based on queue events
Package info
github.com/robuust/laravel-heroku-queue-listener
pkg:composer/robuust/laravel-heroku-queue-listener
Requires
- php: ^8.2
- ext-pcntl: *
- illuminate/cache: ^10.0|^11.0|^12.0|^13.0
- illuminate/contracts: ^10.0|^11.0|^12.0|^13.0
- illuminate/events: ^10.0|^11.0|^12.0|^13.0
- illuminate/queue: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- php-heroku-client/php-heroku-client: ^4.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-03 10:24:17 UTC
README
Autoscale Heroku worker dynos based on Laravel queue events.
Installation
composer require robuust/laravel-heroku-queue-listener
The package uses Laravel package auto-discovery.
PCNTL is required for Laravel's queue:work daemon to handle POSIX signals and enforce job timeouts. Heroku's PHP buildpack enables extensions required by dependencies from the application's committed composer.lock on the next build.
If a shell script launches the worker, use exec php artisan queue:work as its final command to replace the shell with PHP. Heroku sends SIGTERM to all processes in a dyno; exec removes the extra shell process.
Configuration
Set the following environment variables:
HEROKU_API_KEYHEROKU_APP_NAME
Optional:
QUEUE_AUTOSCALER_RELEASE_EVENT(defaults toApp\\Events\\AppReleased)QUEUE_AUTOSCALER_CACHE_KEY(defaults toqueue-autoscaler:current-dynos)QUEUE_AUTOSCALER_CACHE_TTL_SECONDS(defaults to3600)
Publish config if needed:
php artisan vendor:publish --tag=queue-autoscaler-config
HTTP failures and retries
Each scaling attempt has a 2-second connection timeout and a 5-second total timeout. There are no sleeps or immediate HTTP retries in the queue event listener. A non-blocking cache lock allows only one scaling request per app at a time. Each event requests its own worker count; concurrent events that cannot acquire the lock return immediately without queuing another request.
Failed requests set a shared exponential cooldown (30, 60, 120, 240, then
300 seconds, with up to 25% jitter, capped at
300 seconds). A valid Retry-After in seconds or HTTP-date format can extend
that cooldown beyond 300 seconds. Success clears the failure count. A later
queue or configured application release event may try again after the cooldown.
A failed request invalidates the cached worker count because a timed-out PATCH
may already have reached Heroku.
The package registers no scheduled tasks and starts no background retry process. It makes requests only in response to existing queue or application release events. Cooldown expiry alone does not trigger a request. If a worker startup fails and no subsequent event occurs after the cooldown, queued work remains waiting until another event retries the scaling request. Likewise, a request skipped because another request holds the lock requires a subsequent event.
Use a shared persistent cache supporting atomic locks (for example Redis) across all application dynos. Local file or array caches do not coordinate requests between dynos. State keys are scoped by Heroku app name, under the configured cache key; the previous unscoped cached count is ignored.
Failures log the HTTP status, Request-Id, RateLimit-Remaining, Retry-After
and the retry delay when a response is available. Authenticated requests and
raw exception messages are not included. A timeout by itself does not establish
rate limiting: Heroku documents that as HTTP 429 with the rate_limit error ID.
The Platform API documents RateLimit-Remaining, but does not promise a
Retry-After or rate-limit reset header, so retries also work without them.
References: Heroku Platform API and HTTP Retry-After.