How to run Laravel queue workers and the scheduler on your own server

Laravel needs two things running in the background: the scheduler, once a minute, and queue workers, all the time. Here is how to set up both on Ubuntu or Debian with the tools the server already has.

Run both as the site's own user, never as root. A worker that runs as root can change any file on the server if your app has a bug. It also leaves files behind that the site can't write to later (see "If something goes wrong").

Before you start

  • Your app is already deployed. If not, start with our guide on how to deploy Laravel on your own server.
  • The examples use a site called shop in /var/www/shop, owned by the user shop. Change the names to yours. Use the same user that PHP-FPM runs the site as (see one PHP-FPM pool per site).
  • We tested every step on Ubuntu 24.04 (PHP 8.3) and Debian 13 (PHP 8.4) with a new Laravel 13 app.

1. Check the queue setting

Look at QUEUE_CONNECTION in the app's .env file:

grep QUEUE_CONNECTION /var/www/shop/.env

New Laravel apps use database, which keeps jobs in a table of your database. That's fine for most sites. redis works the same way with the steps below. If it says sync, jobs run right away inside the web request, and you don't need workers at all.

Try one worker by hand. It runs the waiting jobs and stops when the queue is empty:

cd /var/www/shop
sudo -u shop php artisan queue:work --stop-when-empty

Each job prints a RUNNING line and then DONE or FAIL.

2. Run the scheduler every minute

The scheduler runs the tasks you list in routes/console.php. The server only has to start php artisan schedule:run once a minute. Pick one of the two ways below, not both, or every task runs twice.

Option A: cron. This is the way the Laravel docs show. Open the site user's crontab:

sudo crontab -u shop -e

Add this line, save and close:

* * * * * cd /var/www/shop && php artisan schedule:run >> /dev/null 2>&1

Option B: a systemd timer. More files, but you get a log of every run in journalctl (step 5). Create /etc/systemd/system/shop-schedule.service:

[Unit]
Description=Laravel scheduler for shop

[Service]
Type=oneshot
User=shop
Group=shop
WorkingDirectory=/var/www/shop
ExecStart=/usr/bin/php artisan schedule:run

And /etc/systemd/system/shop-schedule.timer:

[Unit]
Description=Run the Laravel scheduler for shop every minute

[Timer]
OnCalendar=*-*-* *:*:00
AccuracySec=1s

[Install]
WantedBy=timers.target

AccuracySec=1s matters. Without it, systemd may start the job up to a minute late, and Laravel only runs tasks that are due in the current minute. Turn the timer on:

sudo systemctl daemon-reload
sudo systemctl enable --now shop-schedule.timer
systemctl list-timers shop-schedule.timer

Either way, check it. This lists your tasks and when each one runs next:

sudo -u shop php artisan schedule:list

3. Keep queue workers running with systemd

A worker is a program that never stops on its own. systemd starts it when the server boots and starts it again whenever it exits. Create /etc/systemd/system/shop-queue@.service:

[Unit]
Description=Laravel queue worker %i for shop
After=network.target

[Service]
User=shop
Group=shop
WorkingDirectory=/var/www/shop
ExecStart=/usr/bin/php artisan queue:work --sleep=3 --tries=3 --max-time=3600 --memory=256
Restart=always
RestartSec=5
TimeoutStopSec=300
MemoryMax=512M
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target

What the lines do:

  • User=shop runs the worker as the site user, not root.
  • --max-time=3600 makes the worker stop after an hour, and --memory=256 once it uses more than 256 MB. A fresh worker then takes over, so slow memory leaks never build up.
  • Restart=always starts the worker again after every exit. Don't use on-failure: a worker that stops because of --max-time or a deploy exits normally, and on-failure would leave it stopped.
  • RestartSec=5 waits 5 seconds first. If the worker crashes over and over, systemd would otherwise give up after a few quick tries.
  • MemoryMax=512M is a hard limit from the system, in case a single job uses far too much memory.
  • TimeoutStopSec=300: when you stop the worker, it finishes the job it's working on first. This is how long systemd waits. Make it longer than your longest job.

The @ in the file name lets you start several workers from one file. Start two:

sudo systemctl daemon-reload
sudo systemctl enable --now shop-queue@1 shop-queue@2
systemctl status 'shop-queue@*'

Both should say active (running). One or two workers are enough for most sites. Add more only if jobs wait too long.

Jobs that run longer than 60 seconds are stopped by Laravel's own time limit. If you have such jobs, add --timeout= with a higher number to the ExecStart line, and keep TimeoutStopSec above it.

4. Restart the workers after every deploy

A worker loads your code once and keeps it in memory. After a deploy, it keeps running the old code until it restarts. In our test, a job queued after a code change still ran the old version. Add this as the last line of your deploy steps:

cd /var/www/shop
sudo -u shop php artisan queue:restart

Each worker finishes its current job, then exits, and systemd starts it again with the new code within a few seconds. Laravel passes the restart signal through the cache, so this needs a real cache store such as database (the default) or redis, not array.

You can also restart them with systemd. They still finish the job they're working on first:

sudo systemctl restart 'shop-queue@*'

The scheduler needs nothing: it starts fresh every minute.

5. Check the logs

Everything a worker prints goes to the system journal. Watch it live:

sudo journalctl -u 'shop-queue@*' -f

See the last hour, including starts, stops and crashes:

sudo journalctl -u 'shop-queue@*' --since '1 hour ago'

With the systemd timer from step 2, every scheduler run is logged too:

sudo journalctl -u shop-schedule.service --since today

With cron, the output goes to /dev/null, so look in your app's log instead: storage/logs/laravel.log.

Jobs that failed every try end up in a list. See it, and send them back to the queue once you've fixed the cause:

sudo -u shop php artisan queue:failed
sudo -u shop php artisan queue:retry all

A note on Supervisor

The Laravel docs use Supervisor instead of systemd. It does the same job: start the workers and keep them running. Our Laravel deploy guide has a Supervisor example. Use whichever you know. systemd is already on every Ubuntu and Debian server, so there's nothing extra to install. Just don't run the same workers in both.

If something goes wrong

  • Jobs wait and never run: no worker is running (systemctl status 'shop-queue@*'), or the worker reads a different queue connection than the app writes to. Check QUEUE_CONNECTION again.
  • "laravel.log could not be opened in append mode: Permission denied": someone ran php artisan as root, and it created a log file that belongs to root. We saw the worker crash on this in our test. Fix the owner with sudo chown shop:shop /var/www/shop/storage/logs/*.log, and always use sudo -u shop.
  • Workers still run old code: run queue:restart (step 4).
  • A worker restarts every few seconds: the reason is in journalctl. Often it's a database that isn't reachable, or a wrong path in WorkingDirectory.
  • Scheduled tasks don't run: check schedule:list and the crontab (sudo crontab -u shop -l) or the timer (systemctl list-timers). Times in schedule:list follow the app's timezone, which may not be yours.

Official sources

Next steps: back up the database your jobs write to with our backup guide, and work through the security checklist for a new PHP server.

ServerNest is being built to set up the scheduler and queue workers for each Laravel app, run them as the site's own user, and restart them after every deploy. The product is still in development. Join early access to try it first.

← Back to the security blog