How Jenkins Manages Build Queue Scheduling and Load Balancing: A Deep Dive into the Core Queue Engine

Jenkins manages build queue scheduling and load balancing through the hudson.model.Queue class, which maintains four internal states (waiting, blocked, buildable, pending) and assigns tasks to executors using a consistent-hashing load balancer that can be customized via extension points.

The jenkinsci/jenkins repository contains a sophisticated scheduling engine that orchestrates millions of builds across distributed executor pools. Understanding how Jenkins build queue scheduling and load balancing works is essential for administrators optimizing CI/CD performance and developers writing custom plugins. At the heart of this system lies the hudson.model.Queue class, which coordinates thread-safe state transitions and executor assignment through a series of extensible extension points.

Understanding the Jenkins Queue Architecture

The core queue implementation in hudson/model/Queue.java organizes build tasks into four distinct internal collections, each representing a specific lifecycle stage. All state transitions are protected by a ReentrantLock (Queue.lock) to ensure thread safety, while a Queue.snapshot object provides the UI with consistent read-only views without blocking the scheduling thread.

Internal Queue States and Collections

When a build enters the system, it progresses through these states:

  • waitingList – Items respecting their quiet period before becoming eligible for execution.
  • blockedProjects – Items that cannot run due to upstream/downstream constraints, node-level limits, or a QueueTaskDispatcher veto.
  • buildables – Items ready to be assigned to an idle executor.
  • pendings – Items that have been offered to an executor but have not yet started running.

How the Build Queue Scheduling Flow Works

According to the jenkinsci/jenkins source code, the scheduling engine follows a precise six-step cycle driven by the MaintainTask periodic timer:

1. Task Entry and Quiet Periods

When a build is triggered via Queue.schedule(Task, int), the task is wrapped in a Queue.Item and inserted into the waitingList until its quiet period expires.

2. Maintenance Cycle Execution

A periodic MaintainTask (initialized in Queue's constructor) invokes Queue.maintain(). This method moves items from waiting → blocked → buildable based on resource availability and dispatcher checks.

3. Blocking Checks and Validation

The Queue.getCauseOfBlockageForItem method iterates over all registered QueueTaskDispatcher extensions (QueueTaskDispatcher.all()) and checks node-level constraints via Node.canTake. If any dispatcher returns a non-null CauseOfBlockage, the item remains in the blocked list.

4. Load-Balancing and Executor Assignment

Once an item is ready, the queue creates a MappingWorksheet containing all idle executors as ExecutorChunk objects. The LoadBalancer (default is LoadBalancer.CONSISTENT_HASH) decides which executor(s) will run the task by building a consistent hash of available executors and assigning the task greedily.

The default implementation weights executors by their capacity (ec.size() * 100) to distribute load proportionally:

// Simplified excerpt from LoadBalancer.CONSISTENT_HASH.map(...)
List<ConsistentHash<ExecutorChunk>> hashes = new ArrayList<>(ws.works.size());
for (int i = 0; i < ws.works.size(); i++) {
    ConsistentHash<ExecutorChunk> hash = new ConsistentHash<>(ExecutorChunk::getName);
    List<ExecutorChunk> chunks = ws.works(i).applicableExecutorChunks();
    Map<ExecutorChunk,Integer> toAdd = Maps.newHashMapWithExpectedSize(chunks.size());
    for (ExecutorChunk ec : chunks) {
        toAdd.put(ec, ec.size() * 100);   // weight = #executors
    }
    hash.addAll(toAdd);
    hashes.add(hash);
}
// greedy assignment → Mapping

5. Sorting Buildable Items

Before items are offered to executors, the QueueSorter sorts the buildables list. The default sorter orders items by FIFO, but a custom sorter can be installed via Queue.setSorter.

// QueueSorter.sortBuildableItems (abstract)
public abstract void sortBuildableItems(List<Queue.BuildableItem> buildables);

6. Execution and State Transition

The selected Executor receives a JobOffer, calls start(workUnit), and the WorkUnit runs the Task. When execution begins, Queue.onStartExecuting moves the item from pendings to the LeftItem cache.

Customizing Jenkins Build Queue Behavior

The queue exposes three primary extension points for customizing Jenkins build queue scheduling and load balancing:

Implementing a Custom LoadBalancer

You can override the default consistent-hashing strategy by extending LoadBalancer and registering it with Queue.setLoadBalancer():

@Extension
public class MyLoadBalancer extends LoadBalancer {
    @Override
    public Mapping map(@NonNull Task task, MappingWorksheet ws) {
        // Simple round-robin: pick the first idle executor for every work chunk
        Mapping m = ws.new Mapping();
        for (int i = 0; i < ws.works.size(); i++) {
            ExecutorChunk ec = ws.works(i).applicableExecutorChunks().get(0);
            m.assign(i, ec);
        }
        return m;
    }
}

// Enable it at runtime
Queue q = Jenkins.get().getQueue();
q.setLoadBalancer(new MyLoadBalancer());

Creating a Priority-Based QueueSorter

To change the dispatch order from FIFO to priority-based, implement QueueSorter:

@Extension
public class PrioritySorter extends QueueSorter {
    @Override
    public void sortBuildableItems(List<Queue.BuildableItem> buildables) {
        buildables.sort(Comparator.comparingInt(item -> {
            // Assume each job has a "priority" parameter
            return Integer.parseInt(item.task.getProperty("priority"));
        }));
    }
}

Using QueueTaskDispatcher for Access Control

Implement QueueTaskDispatcher to veto builds based on external conditions like maintenance windows or license availability:

@Extension
public class MaintenanceDispatcher extends QueueTaskDispatcher {
    @Override
    public CauseOfBlockage canRun(Queue.Item item) {
        if (System.currentTimeMillis() < maintenanceEnd) {
            return CauseOfBlockage.fromMessage("Jenkins is in maintenance mode");
        }
        return null; // allow execution
    }
}

Scheduling Builds Programmatically

You can interact directly with the queue from Groovy scripts or management commands:

def job = Jenkins.instance.getItemByFullName('example-job')
def task = job.asJob()          // implements Queue.Task
def queue = Jenkins.instance.queue
queue.schedule(task, 0)         // 0s quiet period → immediate entry

This invokes Queue.schedule(Task,int) and immediately inserts the task into the scheduling pipeline.

Summary

  • Jenkins build queue scheduling is orchestrated by hudson.model.Queue, which maintains thread-safe state transitions across four collections: waitingList, blockedProjects, buildables, and pendings.
  • Load balancing uses a consistent-hashing algorithm by default (LoadBalancer.CONSISTENT_HASH), weighting executors by their capacity to distribute tasks proportionally.
  • Extension points allow customization of scheduling behavior through LoadBalancer, QueueSorter, and QueueTaskDispatcher implementations.
  • State protection relies on Queue.lock (a ReentrantLock) and Queue.snapshot for consistent UI updates without blocking the scheduler.
  • Programmatic access is available via Queue.schedule() for automation and scripted pipeline management.

Frequently Asked Questions

How does Jenkins decide which executor runs a build?

Jenkins uses the LoadBalancer.CONSISTENT_HASH implementation by default, which creates a consistent hash ring of all idle ExecutorChunk objects (grouped by node). The algorithm assigns weights based on executor count (ec.size() * 100) and maps tasks to executors using greedy assignment. You can customize this behavior by implementing the LoadBalancer extension point and registering it via Queue.setLoadBalancer().

What is the difference between blocked and buildable states in the Jenkins queue?

Items in the blockedProjects list cannot run due to external constraints checked by QueueTaskDispatcher implementations or node-level limits (Node.canTake). Items in the buildables list have passed all blocking checks and are ready for immediate executor assignment. The Queue.maintain() method periodically moves items between these states based on current resource availability and dispatcher evaluations.

Can I change the default FIFO ordering of the Jenkins build queue?

Yes. While the default QueueSorter uses FIFO ordering, you can implement a custom sorter by extending hudson.model.queue.QueueSorter and overriding sortBuildableItems(List<Queue.BuildableItem>). Register your implementation with Queue.setSorter() to reorder builds based on priority, job duration estimates, or custom business logic before the load balancer assigns them to executors.

How do I prevent specific jobs from running during maintenance windows?

Implement a QueueTaskDispatcher extension that overrides canRun(Queue.Item). Return a CauseOfBlockage object when your maintenance condition is active, or return null to allow execution. Because Jenkins checks all registered dispatchers before moving items from blockedProjects to buildables, this effectively vetoes builds during the specified window without modifying job configurations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →