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.
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
shopin/var/www/shop, owned by the usershop. 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=shopruns the worker as the site user, not root.--max-time=3600makes the worker stop after an hour, and--memory=256once it uses more than 256 MB. A fresh worker then takes over, so slow memory leaks never build up.Restart=alwaysstarts the worker again after every exit. Don't useon-failure: a worker that stops because of--max-timeor a deploy exits normally, andon-failurewould leave it stopped.RestartSec=5waits 5 seconds first. If the worker crashes over and over, systemd would otherwise give up after a few quick tries.MemoryMax=512Mis 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. CheckQUEUE_CONNECTIONagain. - "laravel.log could not be opened in append mode: Permission denied": someone ran
php artisanas root, and it created a log file that belongs to root. We saw the worker crash on this in our test. Fix the owner withsudo chown shop:shop /var/www/shop/storage/logs/*.log, and always usesudo -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 inWorkingDirectory. - Scheduled tasks don't run: check
schedule:listand the crontab (sudo crontab -u shop -l) or the timer (systemctl list-timers). Times inschedule:listfollow the app's timezone, which may not be yours.
Official sources
- Laravel 13 docs: Queues, with queue workers and deployment and Supervisor configuration
- Laravel 13 docs: Running the scheduler
- systemd man pages: systemd.service (Restart, RestartSec, TimeoutStopSec), systemd.timer (OnCalendar, AccuracySec), systemd.resource-control (MemoryMax) and journalctl
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.