Laravel Vapor runs no long-lived queue:work process; how are queued jobs and scheduled tasks executed there, and what does that change operationally?
answer
- one SQS message, one invocation
- hidden vapor:work command
- SIGALRM throws VaporJobTimedOutException
- vapor:schedule, cache lock, schedule:run
- warm container keeps singletons
basics
~20 sOn Vapor, each SQS message triggers a Lambda invocation that runs the hidden vapor:work command to process that one job, and a per-minute invocation runs vapor:schedule, which calls schedule:run. There are no worker daemons to supervise or restart.
solid answer
~40 svapor-core registers its own connector for the `sqs` queue driver and, on Vapor, fills `queue.connections.sqs` from the environment (`SQS_PREFIX`, `SQS_QUEUE` defaulting to `default`). Dispatching still pushes to SQS, but no `queue:work` loop polls it: each message is delivered to a Lambda invocation that runs the hidden `vapor:work` command, which builds a `VaporJob` from the event's record and hands it to `VaporWorker::runVaporJob()`. That method arms a `SIGALRM` alarm for the job's timeout and throws `VaporJobTimedOutException` when it fires. The scheduler is a per-minute invocation of `vapor:schedule`, which takes a cache lock when the default store is Redis, Memcached, DynamoDB or database and then calls `schedule:run`. Operationally: no Supervisor, no worker restarts after a deploy, concurrency follows Lambda scaling, and every job must finish inside the function's time limit.
code
php · 22 lines<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class GenerateInvoicePdf implements ShouldQueue
{
use Queueable;
public int $timeout = 120;
public int $tries = 3;
public function __construct(public int $invoiceId) {}
public function handle(): void
{
// Render into /tmp, then upload the file to the s3 disk.
}
}go deeper
Recall that Vapor queues use SQS and that no queue:work daemon runs; each job is a function invocation.
Explain the path from dispatch to SQS to a vapor:work invocation, and how vapor:schedule replaces the cron entry for schedule:run.
Show the production traps: the SIGALRM timeout and the function's time limit, warm-container state between jobs, and the scheduler lock's cache-store requirement.
Judge which workloads belong on invocation-per-job processing and which need long-lived workers elsewhere, and what that split costs the team to operate.
## The server model you are leaving On a traditional server, Laravel's queue is processed by `php artisan queue:work`: a **long-running PHP process** that polls a queue connection, runs jobs one after another, and is kept alive by a process monitor such as Supervisor. After each deploy those workers must be told to restart, because they hold the old code in memory. The scheduler is a single cron entry that runs `schedule:run` every minute. **Laravel Vapor** runs on **AWS Lambda**, where there are no long-lived processes you control. vapor-core (2.47) replaces both mechanisms. ## How a queued job runs on Vapor 1. At boot, `VaporServiceProvider` calls `Queue::extend('sqs', ...)` so the `sqs` driver uses Vapor's `VaporConnector`, and on Vapor it fills `queue.connections.sqs` with credentials, `SQS_PREFIX`, `SQS_QUEUE` (default `default`) and the region. 2. Your code dispatches as usual; the job payload is sent to an **SQS** queue. 3. SQS delivers the message to a Lambda invocation of the CLI runtime. The runtime runs the hidden `vapor:work` command, which reads `Records[0]` from the event and builds a `VaporJob`. 4. `VaporWorker::runVaporJob()` calls `app()->forgetScopedInstances()`, arms `pcntl_alarm()` with the job's timeout, increments the attempt counter and runs the job through Laravel's normal worker pipeline, so job middleware, `failed()` and the failed-job store still apply. `vapor:work` accepts `--delay`, `--timeout` and `--tries` (each defaulting to 0) plus `--force`; the platform passes the real values when it invokes the command. Without `--force` it skips work while the app is in maintenance mode. ## Timeouts and the warm container - **Timeouts are enforced by a signal.** A `SIGALRM` handler throws `Laravel\Vapor\VaporJobTimedOutException` (its message says the job will be retried), which ends that attempt. The alarm uses the job's own `$timeout` if set, otherwise the timeout passed to `vapor:work`. - **The Lambda function has its own ceiling.** However generous the job timeout, the invocation cannot outlive the function's configured time limit, so a job that ran for a long time on a server worker must be split up or batched. - **Containers are reused.** The runtime boots the application once per container and loops over invocations (up to `VAPOR_MAX_REQUESTS`, 250 by default). Singletons and static state set by one job can therefore still be there for the next job in that container; only scoped instances are cleared before each job. ## The scheduler on Vapor The platform invokes the hidden `vapor:schedule` command every minute. Its logic: - If the default cache store is `memcached`, `redis`, `dynamodb` or `database`, it tries to claim `vapor:schedule:lock` with `Cache::remember()` for 60 seconds using a fresh UUID, waits until second 0 of the minute, releases the key and calls `schedule:run`. An invocation that did not win the lock exits. - Otherwise (for example the `file` or `array` store) it calls `schedule:run` immediately with no lock, so overlapping invocations can both run the schedule. ## Operational consequences | Concern | Forge-style server | Vapor | |---|---|---| | Worker process | `queue:work` under a process monitor | one `vapor:work` per SQS message | | After a deploy | restart workers | new invocations use the new code | | Concurrency | number of worker processes | Lambda scaling of the queue function | | Longest job | whatever `--timeout` allows | bounded by the function's time limit | | Scheduler | cron runs `schedule:run` | per-minute `vapor:schedule` | | Shared state between jobs | one process, reused | warm container, reused | ## What still applies Jobs are still serialized payloads that run later, outside the request; `ShouldQueue`, `$tries`, `$timeout`, job middleware and failed-job handling behave as in any Laravel app. Jobs must stay idempotent, because an SQS message can be delivered again after a timeout or a crash. What disappears is the operational layer around workers. ## Checking a job before it moves to Vapor 1. Measure its real duration on the current workers and compare it with the queue function's time limit, leaving headroom. 2. Make sure it tolerates running twice, because a timed-out or crashed attempt is retried from SQS. 3. Remove reliance on files another job wrote to local disk; pass S3 keys instead. 4. Look for static caches or singletons that grow across jobs, since a warm container reuses the booted app for many invocations. 5. Set its `$tries` and `$timeout` explicitly rather than inheriting whatever the platform passes. A job that fails this list is a candidate for splitting into smaller jobs, or for staying on a server-based worker.
- Why might a job that behaved on a Forge server start failing with VaporJobTimedOutException on Vapor?`VaporWorker` arms a `SIGALRM` alarm for the job's timeout and throws `VaporJobTimedOutException` when it fires, and the whole invocation is also capped by the Lambda function's time limit. A job that used to run for many minutes under a generous `queue:work --timeout` must be split into smaller jobs or a batch rather than given a bigger number.
- Can state set in one Vapor job leak into the next?Yes. The CLI runtime boots the app once per container and loops over invocations, so singletons and static properties set during one job survive into the next job that container handles. `runVaporJob()` calls `forgetScopedInstances()` before each job, which clears scoped bindings only, so per-job state belongs in `scoped()` bindings or local variables.
- What goes wrong with the scheduler on Vapor if the default cache store is file?`vapor:schedule` only takes its `vapor:schedule:lock` key when the default store is memcached, redis, dynamodb or database. With `file` or `array` it skips the lock and calls `schedule:run` immediately, so two overlapping invocations can both run the schedule. Point the default cache store at a shared driver.
saying these in an interview costs you the question
- Vapor runs php artisan queue:work under Supervisor inside Lambda.
- You must restart queue workers after every Vapor deploy.
- A Vapor job can run for as long as it needs, like a server worker.
- Each Vapor job boots a brand-new application, so singletons never carry over.
- vapor:schedule locks through the cache whichever store is the default.