heyjorgedev / qstash-laravel
A Laravel queue driver for Upstash QStash
Fund package maintenance!
Requires
- php: ^8.3
- illuminate/contracts: ^12.0||^13.0
- illuminate/http: ^12.0||^13.0
- illuminate/queue: ^12.0||^13.0
- illuminate/routing: ^12.0||^13.0
- illuminate/support: ^12.0||^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.32
- nunomaduro/collision: ^8.9
- orchestra/testbench: ^10.0||^11.0
- pestphp/pest: ^4.4||^5.0
- pestphp/pest-plugin-arch: ^4.0||^5.0
- pestphp/pest-plugin-laravel: ^4.1||^5.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-11 10:37:52 UTC
README
A Laravel queue driver for Upstash QStash. Dispatch jobs as you normally would and let QStash deliver them back to your application over HTTP, with no queue:work process to run.
The package also includes a QStash facade to publish messages to any URL, such as another service or a customer's webhook endpoint, and a middleware to receive messages signed by QStash on any route.
How It Works
QStash is a push-based HTTP queue. Instead of a worker polling for jobs, QStash calls your application:
- When a job is dispatched, the driver publishes the serialized job payload to QStash, with a destination URL pointing back at your application.
- QStash makes a signed
POSTrequest to that URL. - The package verifies the
Upstash-SignatureJWT (HS256, signed with your current or next signing key), checking the issuer, expiry, destination URL and SHA-256 hash of the body. - The job is run through Laravel's queue worker, so job events,
$tries,$backoff,$maxExceptions, failed jobs and thefailed()method all work as normal.
Because jobs are pushed to you, your application must be reachable over HTTP by QStash.
Requirements
- PHP 8.3+
- Laravel 12 or 13
Installation
Install the package via Composer:
composer require heyjorgedev/qstash-laravel
The service provider is registered automatically. Optionally, publish the configuration file:
php artisan vendor:publish --tag="qstash-config"
This publishes config/qstash.php:
return [ 'token' => env('QSTASH_TOKEN'), 'current_signing_key' => env('QSTASH_CURRENT_SIGNING_KEY'), 'next_signing_key' => env('QSTASH_NEXT_SIGNING_KEY'), 'endpoint' => env('QSTASH_URL', 'https://qstash.upstash.io'), // Jobs are delivered to "{path}/{connection}/{queue}" on your application. 'path' => env('QSTASH_PATH', 'qstash'), ];
Configuration
Set your credentials, found in the Upstash console, in your .env file:
QUEUE_CONNECTION=qstash QSTASH_TOKEN= QSTASH_CURRENT_SIGNING_KEY= QSTASH_NEXT_SIGNING_KEY=
Then add a qstash connection to your config/queue.php file. Connections use the credentials from config/qstash.php unless they define their own, so the connection may be as small as ['driver' => 'qstash']. Every option is shown below:
'qstash' => [ 'driver' => 'qstash', 'token' => env('QSTASH_TOKEN'), 'current_signing_key' => env('QSTASH_CURRENT_SIGNING_KEY'), 'next_signing_key' => env('QSTASH_NEXT_SIGNING_KEY'), 'queue' => env('QSTASH_QUEUE', 'default'), 'endpoint' => env('QSTASH_URL', 'https://qstash.upstash.io'), 'destination' => env('QSTASH_DESTINATION_URL'), 'retries' => env('QSTASH_RETRIES'), 'retry_delay' => env('QSTASH_RETRY_DELAY'), 'timeout' => env('QSTASH_TIMEOUT'), 'flow_control' => [ 'key' => env('QSTASH_FLOW_CONTROL_KEY'), 'parallelism' => env('QSTASH_PARALLELISM'), 'rate' => env('QSTASH_RATE'), 'period' => env('QSTASH_PERIOD'), ], 'after_commit' => false, ],
The endpoint option is the QStash API base URL. Change it if your QStash instance lives in another region (for example, https://qstash-us-east-1.upstash.io) or when using the local development server. Each region has its own token and signing keys, so always use the credentials belonging to the region of your endpoint. To publish through several regions, define a connection per region.
The destination option is the public base URL QStash should call. It defaults to your app.url. Jobs are delivered to {destination}/{path}/{connection}/{queue}, for example:
https://example.com/qstash/qstash/default
The retries and retry_delay options are sent to QStash as the Upstash-Retries and Upstash-Retry-Delay headers, and control how QStash redelivers a job when your application fails to respond successfully. The delay is in milliseconds and may be a QStash expression such as pow(2, retried) * 1000. When omitted, your QStash plan's defaults apply.
The timeout option is how long, in seconds, QStash waits for your application to respond before considering a delivery failed. A job's own $timeout property takes precedence. See Long-Running Jobs.
Flow Control
QStash delivers jobs as fast as it can, so dispatching thousands of jobs at once results in thousands of requests competing with your users for PHP workers. Use the flow_control option to limit how many jobs are delivered at the same time (parallelism), and how many are delivered per period (rate). A key and at least one limit are required:
QSTASH_FLOW_CONTROL_KEY=my-app QSTASH_PARALLELISM=10
Every connection using the same key shares the same limits. To limit queues independently, define a connection per queue, each with its own key.
Usage
Dispatch jobs exactly as you would with any other queue driver:
ProcessPodcast::dispatch($podcast); ProcessPodcast::dispatch($podcast)->onQueue('emails'); ProcessPodcast::dispatch($podcast)->delay(now()->addMinutes(10));
Delayed jobs use QStash's Upstash-Not-Before header. The maximum delay depends on your QStash plan.
Retries & Failures
Laravel's retry semantics apply. When a job is released, either manually or via $backoff after an exception, the driver publishes it to QStash again with the appropriate delay.
The endpoint responds with a 2xx status once Laravel has handled the job, whether it succeeded, was released or failed, so QStash does not duplicate Laravel's retries. QStash's own retries only kick in if a request fails without Laravel handling it, such as when the PHP process crashes or times out. Those deliveries count towards the job's attempts, and their timing can be tuned with the retries and retry_delay options.
Publishing Messages
Beyond queued jobs, you may publish a message to any URL using the QStash facade. QStash delivers it with retries, which makes it a reliable way to call another service or send webhooks. Arrays are sent as JSON, and the QStash message ID is returned:
use HeyJorgeDev\QstashLaravel\Facades\QStash; $messageId = QStash::publish('https://billing.example.com/invoices', [ 'invoice_id' => $invoice->id, ]);
The message may be customized before it is published:
QStash::withHeaders(['X-Signature' => $signature]) ->delay(now()->addHour()) ->retries(5, delay: 'pow(2, retried) * 1000') ->timeout(30) ->callback(route('webhooks.delivered')) ->failureCallback(route('webhooks.failed')) ->deduplicate("invoice-paid-{$invoice->id}") ->flowControl('customer-webhooks', parallelism: 10) ->publish($customer->webhook_url, $payload);
| Method | Description |
|---|---|
withHeaders($headers) |
Headers QStash includes in its request to the destination. |
withBody($content, $contentType) |
Send a raw body, such as XML, instead of JSON. |
method($method) |
The HTTP method QStash uses to call the destination. Defaults to POST. |
delay($delay) |
Seconds, or a DateTimeInterface, to delay delivery by. |
retries($times, $delay) |
How many times to retry, and the milliseconds (or expression) to wait in between. |
timeout($timeout) |
Seconds, or a duration such as 5m, to wait for the destination to respond. |
callback($url) |
A URL QStash calls with the response of the destination. |
failureCallback($url) |
A URL QStash calls once every attempt has failed. |
deduplicate($id) / deduplicateByContent() |
Ignore duplicate messages within QStash's deduplication window. |
flowControl($key, $parallelism, $rate, $period) |
Limit how many messages sharing a key are delivered at once, or per period. |
Since the facade uses Laravel's HTTP client, you may use Http::fake() in your tests to fake publishing.
Receiving Messages
Callbacks, schedules, and messages you publish to your own application are signed by QStash. To verify the signature on any route, use the qstash middleware. Requests with a missing or invalid signature receive a 403 response:
Route::post('/webhooks/delivered', DeliveredWebhookController::class)->middleware('qstash');
The signature covers the URL QStash called. If your application runs behind a load balancer or proxy, make sure trusted proxies are configured so the URL your application sees matches the URL you published to. Like the job route, these routes should not be protected by CSRF verification.
Maintenance Mode
Like a queue worker, the driver does not run jobs while your application is down for maintenance. Jobs delivered during maintenance are handed back to QStash to be delivered again a minute later, without counting as an attempt. The delivery route is excluded from the maintenance mode middleware automatically.
Long-Running Jobs
Jobs run inside an HTTP request, so they are bound by every timeout along the way: QStash's, your load balancer's or proxy's, and PHP's max_execution_time. When QStash stops waiting before the job is done, it considers the delivery failed and delivers the job again while the first attempt is still running.
To avoid this, the driver asks QStash to wait for as long as the job's $timeout (or the connection's timeout), up to the maximum allowed by your QStash plan. Make sure your proxy and PHP limits are at least as long, and consider ShouldBeUnique or the WithoutOverlapping middleware for jobs that must never run twice concurrently. The job $timeout is not enforced by the driver, since there is no pcntl alarm in an HTTP context. Jobs that run for many minutes belong on a different queue connection.
Things to Know
- Queue size.
Queue::size()and related methods always return0, as QStash does not expose these counts for published messages. - Middleware. The route is registered without the
webmiddleware group, so CSRF protection does not apply. Do not put it behind authentication: the signature is the authentication. - Invalid signatures. Requests with a missing or invalid signature receive a
403response.
Local Development
QStash cannot reach localhost, so you have two options:
-
Use a tunnel. Expose your application with a tool such as ngrok, Expose or cloudflared, then set
QSTASH_DESTINATION_URLto the tunnel URL. -
Run QStash locally. Start Upstash's local QStash server:
npx @upstash/qstash-cli dev
Then set
QSTASH_URL=http://127.0.0.1:8080and use the token and signing keys it prints.
Testing
composer test
The integration tests deliver real jobs through the local QStash development server to a workbench application. They start both servers themselves on ports 18080 and 18001, and require Node.js (npx):
composer test-integration
Changelog
Please see CHANGELOG for more information on what has changed recently.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.