How Jenkins Handles Concurrent Build Execution Using Executor and RunnerStack

Jenkins handles concurrent build execution by assigning each build to a dedicated Executor thread on a node, while RunnerStack maintains a per-thread mapping so the system can always identify which RunExecution is active on the current thread.

Jenkins, as implemented in the jenkinsci/jenkins repository, scales build throughput by parallelizing work across multiple executor threads rather than running jobs sequentially. Understanding how Jenkins concurrent build execution actually works requires examining the interaction between the Executor class that hosts build threads and the RunnerStack utility that tracks them.

Executor Lifecycle and Thread Assignment

When a node is created, the number of executors it can host is stored in Computer.numExecutors. For each configured slot, a hudson.model.Executor object is instantiated, but the underlying Java thread is lazy-started only when the queue assigns a WorkUnit to it via Executor.start(WorkUnit).

Inside Executor.run() and State Locking

Once started, the executor thread enters Executor.run(). According to the Jenkins source code in core/src/main/java/hudson/model/Executor.java, this method performs the following steps:

  1. Acquires the internal ReadWriteLock referred to as lock to protect mutable state such as executable, workUnit, and asynchronousExecution.
  2. Asks the queue to callWithLock so that the transition from idle to building is atomic.
  3. Creates the Executable by invoking createExecutable() and stores it.
  4. Calls queue.execute(executable, task), which finally delegates to the build’s RunExecution.run() method.

The executor therefore isolates each build in its own Java thread, while the Queue guarantees that two executors never pick the same WorkUnit. Concurrency is limited only by the number of executors configured on the node.

RunnerStack: Per-Thread RunExecution Bookkeeping

While a build is running, Jenkins must know which RunExecution is associated with the current thread—for example, when handling checkpoints, processing interruptions, or rendering the “who is building this?” UI. The hudson.model.RunnerStack class solves this by keeping a WeakHashMap<Executor, Stack<RunExecution>>.

Pushing the Current RunExecution on Build Start

When a build starts, Run.onStartBuilding() pushes the current RunExecution onto the stack:

// Run.java
protected void onStartBuilding() {
    …
    if (runner != null)
        RunnerStack.INSTANCE.push(runner);
    …
}

(source: Run.java

Popping the Stack on Build Completion

When the build ends, Run.onEndBuilding() pops the stack, finalizes checkpoint bookkeeping, and clears the runner reference:

protected void onEndBuilding() {
    …
    if (runner != null) {
        runner.checkpoints.allDone();   // finish checkpoint bookkeeping
        runner = null;
        RunnerStack.INSTANCE.pop();
    }
    …
}

(source: Run.java

Querying the Current Build Context

RunnerStack.peek() is used throughout Jenkins core—such as in checkpoint handling inside Run.reportCheckpoint—to obtain the currently executing build for the calling thread:

// Run.java
Run<?, ?>.RunExecution exec = RunnerStack.INSTANCE.peek();

(source: Run.java

Concurrency Safety Mechanisms

All mutable fields in Executor are guarded by its internal ReadWriteLock. The queue interacts with the executor under Queue.callWithLock, ensuring that only one thread can modify an executor’s state at a time.

RunnerStack itself is a synchronized structure, and because the underlying map uses weak references, finished executors do not leak memory. These layers ensure that Jenkins concurrent build execution remains safe without requiring plugins to manage their own locking.

Querying and Controlling Executors in Practice

Query an Executor’s State via the Remote API

You can inspect whether an executor is idle or running a build using a Groovy script:

import jenkins.model.Jenkins

def node = Jenkins.instance.getComputer('my‑agent')
node.getExecutors().each { exec ->
    println "Executor #${exec.getNumber()} on ${node.getName()}: " +
            (exec.isIdle() ? 'idle' : "running ${exec.getCurrentExecutable().getFullDisplayName()}")
}

Uses: Executor.isIdle(), Executor.getCurrentExecutable() — see Executor.java.

Using RunnerStack Inside a Plugin to Find the Current Build

Plugins can discover the active build for the current thread without explicit reference passing:

import hudson.model.RunnerStack;
import hudson.model.Run;

Run<?, ?>.RunExecution exec = RunnerStack.INSTANCE.peek();
if (exec != null) {
    Run<?,?> build = exec.getBuild();
    // e.g. log the job name
    listener.getLogger().println("Current build: " + build.getFullDisplayName());
}

Reference: RunnerStack.peek() implementation — see RunnerStack.java.

Manual Creation of a WorkUnit for Testing

For integration tests or advanced automation, you can manually create a WorkUnit and assign it to an idle executor:

import hudson.model.Executor;
import hudson.model.Queue;
import hudson.model.Queue.WorkUnit;

// Assume 'computer' is a Computer instance with at least one idle executor
Executor exec = computer.getExecutors().stream()
                       .filter(Executor::isIdle)
                       .findFirst()
                       .orElseThrow(() -> new IllegalStateException("No idle executor"));

WorkUnit wu = new WorkUnit(/* task */, /* actions */, /* actions */);
exec.start(wu);   // executor thread will be started on demand

Key method: Executor.start(WorkUnit) — see Executor.java.

Summary

  • Executor threads are created lazily via Executor.start(WorkUnit) and run builds in isolated Java threads, with state protected by a ReadWriteLock.
  • The Queue assigns WorkUnits atomically using Queue.callWithLock, ensuring no two executors claim the same work.
  • RunnerStack provides a synchronized, per-thread stack of RunExecution instances, enabling components to query the current build via peek().
  • Builds push their RunExecution onto the stack in Run.onStartBuilding() and pop it in Run.onEndBuilding(), preventing stale context.
  • Concurrency is bounded only by Computer.numExecutors, making scalability a matter of node configuration.

Frequently Asked Questions

What limits how many builds Jenkins can run concurrently on one node?

The hard limit is Computer.numExecutors, which defines how many hudson.model.Executor instances (and therefore Java threads) are created for that node. The Queue will not assign more WorkUnits than there are idle executors available.

How does Jenkins prevent race conditions when an executor picks up a new build?

Jenkins uses Queue.callWithLock to perform the idle-to-building transition atomically. Inside Executor.run(), the executor acquires its internal ReadWriteLock before mutating state, so the Queue and the executor thread never corrupt shared data.

Why does Jenkins use RunnerStack instead of passing RunExecution references directly?

RunnerStack provides a global, thread-local lookup via WeakHashMap<Executor, Stack<RunExecution>>. This design lets internal systems—such as checkpoint reporting and the “who is building this?” UI—discover the current execution context without threading explicit references through every API boundary.

Can a plugin safely call RunnerStack.peek() during a build?

Yes. As implemented in the Jenkins source code, RunnerStack is synchronized and intended for this exact purpose. If the calling thread is an executor running a build, peek() returns the active RunExecution; otherwise it returns null.

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 →