Deep Dive into Laravel Horizon — what goes on behind the scenes

Zechariah Campbell
6 min readFeb 4, 2018

--

Laravel Horizon is a simple way to get started with Laravel Queues by reducing the amount of configuration to spawn and manage queue workers. However, understanding how the queue worker processes are created and in what order jobs get processed can be a little unclear when starting out, especially if you are new to Queues altogether.

In this article I will dive into and explain:

  • How many worker processes are being spawned by Laravel Horizon?
  • If I have more than one queue, such as a high and default queue
    — how many worker processes are created by Laravel Horizon?
    — what order are jobs executed?

Horizon has an option called balance which accepts false , simple , or auto. The balance option directly affects worker creation and job execution.

Let’s start with the easiest option first, balance=false

balance=false

Documentation says:

When the balance option is set to false, the default Laravel behavior will be used, which processes queues in the order they are listed in your configuration.

In the case of balance=false here is how worker processes are created:

  1. Create worker processes equal to the number you have set processes to in config/horizon.php

Job execution order:

  • The workers will always process jobs in the order of how you define the queues in config/horizon.php .

Examples? Ya, that might help

// config/horizon.php
...
'local' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['high', 'default'],
'balance' => 'false',
'processes' => 1,
'tries' => 0
,
],
],
...

With the above config, notice we have two queues specified (high, default)

We also have balance=false and processes=1

To get a better understanding of what processes are being created we can login to our Homestead server

  1. Login to Homestead with homestead ssh . This only works if you are using homestead AND you setup this function.
  2. Once you are in the server, you can run top to view a list of all processes.

In the image below, each line is a process on the server. I cut out a bunch of useless information like the PID. Here you just see the command that Horizon is running. If you are curious how I got this information, Check out my other post.

balance=false AND processes=1

artisan horizon:superviso is the process responsible for managing the worker processes.

artisan horizon:work is our actual worker process created by supervisor.

Notice the supervisor has a param of --max-processes=1 and --min-processes=1 The min-processes will become more important with the other strategies we will talk about later.

And notice the horizon:work process has a param of --queue=high,default This means it will process ALL high jobs first, then default ones. This will also change in the other strategies.

What happens when I have a few jobs in both queues? What order are they processed?

I have a small script that creates some simple test jobs for both queues just to demonstrate the order jobs are being processed. Let’s take a look!

balance=false AND processes=1 while adding jobs to see what order they are processed.
  1. I added some jobs to the queue — 5 default z:1, z:2, z:3, z:4, z:5
    — 1 high high1
  2. The queue starts by processing a high priority job (high1) then a default job ( z:1)
  3. When the worker starts started processing z:2, I manually created 5 more default jobs and 1 more high job.
  4. The worker finished processing z:2 then started on the high2 job.

Keep in mind, I only have one worker process in this example. If I have 2 or more workers, you would see multiple jobs starting at the same time.

balance = simple

Documentation says:

The simple strategy […] splits incoming jobs evenly between processes.

In the case of balance=simple here is how worker processes are created:

  1. Always create 1 worker process for each queue, even if you specify fewer processes than queues in your config.
  2. If there are any left over processes, create 1 new process for each queue in the order of how the queues are specified in the config.

For example:

'queue' => ['high', 'default'],
'balance' => 'simple',
'processes' => 1,

will result in 2 processes, NOT 1 process. 1 process is created for each queue in the queue config.

balance=simple AND processes=1

And this:

'queue' => ['high', 'default', 'emails'],
'balance' => 'simple',
'processes' => 5,

will result in 5 total.

  • 2 for high
  • 2 for default
  • 1 for emails

Why does this matter? Imagine you want your works to process everything in the high priority queue first before moving onto the default queue. If that is the case, stick to balance=false

balance = auto

Documentation says:

The auto strategy adjusts the number of worker processes per queue based on the current workload of the queue. For example, if your notifications queue has 1,000 waiting jobs while your render queue is empty, Horizon will allocate more workers to your notifications queue until it is empty.

In the case of balance=auto here is how worker processes are created:

  1. Always create 1 worker process for each queue, even if you specify fewer processes than queues in your config.
  2. If there are any left over processes, create 1 new process for each queue in the order of how the queues are specified in the config.
  3. The worker processes are then reassigned to different queues based on which one has jobs in the queue. There will still always be at least 1 process per queue.

For example:

'queue' => ['high', 'default'],
'balance' => 'auto',
'processes' => 1,

will result in 2 processes, NOT 1 process. 1 process is created for each queue in the queue config.

balance=auto AND processes=1

And this:

'queue' => ['high', 'default', 'emails'],
'balance' => 'auto',
'processes' => 5,

will result in 5 total.

  • 2 for high
  • 2 for default
  • 1 for emails

However, 2 of the workers will be reassigned to whichever queue has the most jobs. For example, if the emailsqueue has 1,000 jobs, the high queue has 0 jobs, and the defaultqueue has 10 jobs, Horizon you will probably reassign the workers like so:

  • 1 for high
  • 1 for default
  • 3 for emails

Notice the high priority queue still has 1 worker even though there are 0 jobs for that queue.

Here is another example: Let’s have 2 queues and 6 processes. We add 1,000 jobs to the default queue, then after a minute, add 1,000 high priority jobs and see how Horizon reassigns workers to queues with more jobs.

'queue' => ['high', 'default'],
'balance' => 'auto',
'processes' => 6,

After adding 1,000 default jobs:

balance=auto AND processes=6 AND added 1,000 default priority jobs while there is only 1 high priority job

After adding 1,000 high jobs while there are still 980 default jobs:

balance=auto AND processes=6 AND added 1,000 high priority jobs while there are about 980 default priority jobs

Note: The balance=auto feature won’t do any automatic switching unless you have excess processes . If you have 10 queues and processes set to 11, 1 worker process will be able to move around freely between queues.

In both simple and auto: If you setup 10 queues and set your processes to 1, you will actually have 10 processes running. Even if you set your processes to 9, you will still have 10 running, 1 for each queue.

If you are curious how I came to these conclusions, check out my other post which is random notes about me testing each case.

Can I change min-processes?

If you are like me, you might be thinking, min-processes is just a config option on the horizon command. Can’t I change that and alter how Horizon spawns my workers. In your config/horizon.php, you can add this to the config:

'min-processes' => 0

Or this:

'min-processes' => '5',

The horizon command does get updated to use this config, however, it seems to have zero effect on how processes are spawned.

--

--