Skip to main content
A queued job is work that runs outside the request, such as sending an order confirmation. Laravel keeps queued jobs in a queue, the database by default, and php artisan queue:work runs them in a background worker: a process that runs from the same image as the web process with its own start command. Laravel’s scheduler runs in a second background worker.

At a glance

What this guide adds to the application from Deploy a Laravel application:

Environment variables

The workers read the same variables as the web process, listed in Deploy a Laravel application.

Before you begin

Create a job

Create a job class:
Replace the generated class with a job that takes an order ID and logs a line:
app/Jobs/SendOrderConfirmation.php
Queue it from a controller or anywhere else with SendOrderConfirmation::dispatch($order->id). Laravel 13’s queue connection is database unless QUEUE_CONNECTION says otherwise, and the deploy guide’s migrations created the jobs table, so the job waits in PostgreSQL with no other configuration. Commit and push the job.

Run the worker on Sevalla

1

Create the background worker

On the application’s Processes tab, click Create process. Choose Background worker, enter Queue worker as the Name and this Custom start command:
Choose a Resource and click Create process.
2

Deploy

Click Deploy in the undeployed changes bar at the bottom of the page.
The Create process sheet with Background worker selected, the Name and Custom start command fields, the resource and the scaling settingsThe Create process sheet with Background worker selected, the Name and Custom start command fields, the resource and the scaling settings
One worker instance runs one job at a time. To run more at once, add instances in the process’s Scaling section; each instance takes its own jobs from the queue. See Scaling.

Stop the worker cleanly on deployments

Sevalla runs a custom start command with /bin/sh -l -c. exec replaces that shell with the worker, so the worker receives the SIGTERM that Sevalla sends when it stops an instance, for example during a deployment: it finishes the job it’s running, logs Worker STOPPED Interrupted, and exits. The worker handles the signal with PHP’s pcntl extension, which Ingot installs for Laravel. Without exec, the shell receives the signal and ignores it. The worker keeps taking jobs until Sevalla stops the instance, 30 seconds after the shutdown began, often in the middle of a job. That job stays reserved until retry_after (90 seconds) has passed, and then fails with App\Jobs\SendOrderConfirmation has been attempted too many times. Sevalla sends SIGTERM 5 seconds after the shutdown begins, so a job has about 25 seconds to finish. Split longer work into smaller jobs, or start the worker with --tries=3 so that a job stopped with its instance runs again. See How processes run.

Use Redis for the queue

The database queue works without another service, but every worker queries the jobs table for new jobs, every 3 seconds while the queue is empty. A Redis queue takes that load off PostgreSQL.
1

Create the database

In the sidebar, click Databases, then click Create database. Under Database, choose Redis, or Valkey: this guide works with both. Enter storefront-redis as the Display name, choose the same Data center as the application and a Size, then click Create database.
2

Connect it to the application

In the sidebar, click Applications, then click the application. Click the Networking tab. Under Connected services, click Add internal connection. Choose storefront-redis as the Service, turn on Add environment variables to the application, and click Add internal connection. Sevalla adds REDIS_HOST, REDIS_PORT, REDIS_PASSWORD and REDIS_URL, the names Laravel’s config/database.php reads. See Connect an application.
3

Switch the queue connection

Click the Environment variables tab, then click Add environment variables. Add QUEUE_CONNECTION with the value redis, and click Add environment variables.
4

Deploy

Click the Overview tab, then click Deploy now under Recent deployments. Check the Branch and click Deploy.
Laravel talks to Redis with the redis PHP extension (phpredis), its default client, which Ingot installs for Laravel. It reads the valkey:// URL that Sevalla adds for a Valkey database too. Jobs still waiting in the jobs table stay there: let the worker finish them before you switch, or run php artisan queue:work database --stop-when-empty in the web terminal to work through them.

Run the scheduler

Laravel’s scheduler runs the tasks you define in routes/console.php. Add one that runs every minute, so you can see it work:
routes/console.php
Commit and push the change, then run the scheduler in its own background worker:
1

Create the background worker

On the application’s Processes tab, click Create process. Choose Background worker, enter Scheduler as the Name and exec php artisan schedule:work as the Custom start command. Choose a Resource, keep the instances at 1, and click Create process.
2

Deploy

Click Deploy in the undeployed changes bar at the bottom of the page.
Keep the scheduler at 1 instance: each instance runs every task. Use a background worker with schedule:work rather than a cron job that runs php artisan schedule:run every minute:
  • schedule:work checks the schedule at the start of every minute in a process that’s already running. A cron job starts a new instance for each run and boots Laravel before it checks: a cron job that runs only the scheduler takes 4 to 9 seconds a run.
  • schedule:work starts each minute’s tasks in a process of their own, so a task that takes 5 minutes doesn’t hold up the next minute’s tasks. A cron job skips its next run while the previous one is still active, the default of its If the previous run is still active setting.
A cron job costs only for the seconds each run takes, so when every task is short and a few seconds’ delay doesn’t matter, you can create a Cron job with the start command php artisan schedule:run and the schedule * * * * * instead.

Update the Dockerfile

If the application builds from the Dockerfile in the deploy guide, install the pcntl extension, which lets the worker stop cleanly, and the redis extension for the Redis queue. Ingot installs both on its own. Change the first RUN line:
Dockerfile
The ldd line finds the libraries the redis extension loads, so the final stage installs them with no other change. The start commands are the same with both build strategies: the image has /bin/sh, and php is on the PATH of the login shell Sevalla starts them with.

Verify

  1. When the deployment’s status is Deployed, click the Web terminal tab, and in the shell that opens in the Web process, queue a job:
  2. Click the Logs tab and look at the Queue worker process’s runtime logs. They show the job running, the line it logs, and DONE:
    Runtime logs
  3. The Scheduler process’s runtime logs show a line every minute:
    Runtime logs
If no job runs, read the worker’s runtime logs, and check that QUEUE_CONNECTION is the same for the web process and the worker: both read it from the application’s environment variables. To see jobs that failed, run php artisan queue:failed in the web terminal.

Check from a terminal

These checks need no dashboard. API, H, APP_ID and WEB_ID are set as in Deploy with the API.
  1. Queue a job in the web process with Run a command in a process (execProcessCommandV3), the API’s counterpart of the web terminal. The command runs without a shell, as a list of arguments, and the key needs write access to the application:
  2. A few seconds later, the line the job logs, from Get application runtime logs (getApplicationRuntimeLogsV3), which searches the last hour by default:
  3. The scheduler’s last run of inspire:
CLI: sevalla apps processes exec $WEB_ID --app-id $APP_ID --timeout 30 -- php artisan tinker --execute 'App\Jobs\SendOrderConfirmation::dispatch(42);', then sevalla apps logs runtime $APP_ID --search "order 42". MCP: sevalla_api_update with execProcessCommandV3, and search_runtime_logs.

Deploy with the API

The same steps as Run the worker on Sevalla and Run the scheduler, as calls to the Sevalla API. APP_ID, WEB_ID and H1_ID are the application, its web process and the H1 size from Deploy with the API in the deploy guide, with API and H set as there. Push the job and the schedule first: the deployment builds the branch as it is.
1

Create the two workers

Create process (createProcessV3) with type worker needs the size and a scaling_strategy. Keep exec in both start commands:
CLI: sevalla apps processes create $APP_ID --name "Queue worker" --type worker --resource-type $H1_ID --command "exec php artisan queue:work", and the same for Scheduler. MCP: sevalla_api_create with createProcessV3 and the same body.
2

Deploy

Trigger deployment (triggerDeploymentV3):
CLI: sevalla apps deployments trigger $APP_ID. MCP: trigger_deployment.
3

Wait for the deployment

Poll Get deployment (getDeploymentV3) until the status is final: success, failed, cancelled or skipped. The loop keeps polling through a failed request and stops after 30 minutes:
CLI: sevalla apps deployments get $DEPLOYMENT_ID --app-id $APP_ID. MCP: get_deployment.
To move the queue to Redis, create and connect the database and add its variables as in Cache Laravel with Redis, with QUEUE_CONNECTION=redis in place of CACHE_STORE=redis, then deploy. Then run the checks in Check from a terminal.