Groovy CPS, Serialization, @NonCPS, Pipeline State, Restarts, and Common Pipeline Programming Pitfalls: Concepts, Architecture, and Mental Model
Understand why Jenkins transforms Pipeline Groovy into continuation-passing style, what must be serializable at suspension points, how persisted run state resumes after restart, and where @NonCPS changes the execution contract.
Learning objectives
- Explain CPS transformation, continuations, suspension points, serialized Pipeline state, and rehydration after restart.
- Distinguish CPS-transformed Pipeline code from native Java/Groovy methods and @NonCPS helpers.
- Identify which variables and objects can become part of persisted Pipeline state and why non-serializable objects can fail.
- Explain why Pipeline Groovy is controller-side orchestration even when a node block allocates an agent.
- Separate durable Jenkins run state from agent workspace files and external side effects.
1. Why ordinary Groovy intuition is not enough
Chapter 12 used steps such as sleep,
input, retry, and waitUntil.
Those steps can pause a run for seconds or hours, and the Jenkins
controller may restart while the Pipeline is paused. A normal
in-memory script would lose its call stack and local variables.
Jenkins solves that problem by transforming most Pipeline Groovy
into a resumable program.
This durability is powerful, but it creates constraints. A local
variable that looks harmless in ordinary Groovy may become part of
the continuation that Jenkins must serialize. A closure passed into
a native Groovy method may cross from CPS-transformed code into
non-CPS code. Heavy loops may consume controller CPU even when they
appear inside node. Understanding these boundaries
turns mysterious Pipeline failures into diagnosable state
transitions.
2. Mental model: source → CPS → persisted continuation → resume
Jenkins first compiles the Pipeline script, but most Pipeline code is transformed into continuation-passing style (CPS). Instead of relying on a normal JVM stack frame that disappears on restart, Jenkins represents the next continuation explicitly. When an asynchronous Pipeline step suspends, Jenkins can persist the relevant program graph. After a restart, the run is loaded, persisted state is rehydrated, step execution is restored, and the continuation can proceed.
flowchart TD
A[Jenkinsfile at exact SCM revision] --> B[Groovy parse / compile]
B --> C[CPS transformation]
C --> D[Continuation and Pipeline state]
D --> E{Asynchronous step / suspension}
E -->|persist| F[Build run state + program.dat]
F --> G[Controller restart]
G --> H[WorkflowRun reload / rehydration]
H --> I[Step onResume / continuation resumes]
I --> J[Next Pipeline step]
C --> K[@NonCPS method]
K --> L[Native Groovy execution on controller]
L --> M[Serializable summary returned to CPS code]
The side path matters: @NonCPS methods are compiled
with ordinary Groovy semantics rather than CPS semantics. They may
use non-serializable objects locally, but they must finish
synchronously, must not call Pipeline steps, and should return only
compact serializable values to the CPS program.
3. What is CPS-transformed?
| Code category | CPS? | Operational consequence |
|---|---|---|
| Most Jenkinsfile / Shared Library Groovy | Yes | Can suspend; state may need serialization |
| Pipeline steps that take blocks | Yes | Participate in durable flow graph |
| Java/JDK/Groovy runtime bytecode | No | Runs natively; cannot call back into CPS arbitrarily |
| Pipeline-script constructors | No | Must not invoke CPS-transformed methods/steps |
@NonCPS methods |
No | No Pipeline steps; no suspension; return safe values |
A legal direction is CPS code calling a normal helper and receiving a plain result. The dangerous direction is native/non-CPS code calling a CPS-transformed closure or Pipeline step.
4. Suspension points and serialization
Many asynchronous steps can suspend: sleep,
input, agent allocation, shell steps, and many
plugin-provided steps. At a persistence point, variables reachable
from the continuation may need to be serialized. Primitive values,
strings, and simple lists/maps of serializable values are usually
safe. Runtime handles such as regex matchers, open streams,
iterators, parser internals, or plugin objects may not be.
def state = [operation: 'cps-demo-001', attempt: 1]
echo "before=${state}"
sleep 3
echo "after=${state}"
This small map is intentionally boring. Boring state is good durable state: compact, inspectable, and easy to reason about across a restart.
5. program.dat is internal run state
The Pipeline: Groovy engine persists the program graph under the
build record, commonly including a program.dat file.
Its exact internal representation is not an operator API. You may
verify that the build/run has durable state and record file metadata
in a disposable controller, but do not edit or transplant
program.dat as a repair technique.
# Read-only inspection on the disposable controller only
find "$JENKINS_HOME/jobs" -path '*/builds/*/program.dat' -type f -printf '%p %s bytes\n' 2>/dev/null | tail
6. @NonCPS: a narrow synchronous boundary
A good @NonCPS helper performs bounded pure computation
and returns a serializable summary:
@NonCPS
String extractBuildId(String text) {
def matcher = text =~ /build-(\d+)/
return matcher.find() ? matcher.group(1) : 'none'
}
def id = extractBuildId('release build-42 ready')
echo "id=${id}"
The regex matcher never escapes the native helper. The returned
String is safe to retain. By contrast, putting
echo, sh, sleep,
node, or another Pipeline step inside the annotated
method violates the execution contract.
7. Groovy logic still consumes controller resources
Pipeline orchestration code executes in the controller process. A
node block allocates an executor/workspace for steps
that need an agent, but an expensive Groovy sort, JSON
transformation, or large in-memory aggregation is still
controller-side unless you deliberately offload it.
node('lab-linux') {
// Agent-side process: appropriate for substantial computation.
sh './scripts/summarize-results.sh results.json > summary.txt'
def summary = readFile('summary.txt').trim()
echo "summary=${summary}"
}
Keep the Pipeline as glue: call tools on agents, retain only the compact result needed for orchestration, and archive larger evidence as files rather than carrying it through the CPS graph.
8. Restart durability does not make external side effects atomic
A resumed Pipeline remembers its Jenkins program state, but Jenkins cannot magically roll back or transactionally restore a package registry, deployment target, ticketing system, or database. If an external API performed an action and Jenkins lost the response before persisting the next continuation, the resumed run may not know whether the action happened.
Use immutable operation IDs, target-side idempotency keys, target-state verification, and compensation logic. Chapter 12’s side-effect guard remains essential even after you understand CPS internals.
9. Read-only evidence before modification
- Record Jenkins core, Java, Pipeline: Groovy, Pipeline: Supporting APIs, and Script Security versions.
- Record job full name, build number/URL, source/Jenkinsfile SHA, current stage/step, node/workspace, and build result.
- Preserve the full exception class and the first useful stack frames before changing code.
- Record whether a suspension point occurred immediately before the failure.
- Record controller heap/CPU or thread evidence if the symptom is performance-related.
- Verify external target state independently before retrying a side effect.
10. Durability settings are a trade-off, not a bug fix
Jenkins can trade disk I/O for Pipeline survivability using
durability settings. Lower-durability modes persist program state
less frequently. That can change when a serialization error becomes
visible, but it does not make a non-serializable program correct. Do
not switch to a performance-optimized durability mode merely to
suppress NotSerializableException.
Knowledge check
Why can a local variable trigger a serialization failure?
If it remains reachable when Pipeline suspends, Jenkins may need to serialize it as part of the continuation; non-serializable runtime objects can therefore fail the run.
What is the safe role of @NonCPS?
A bounded synchronous helper that performs native Groovy/Java computation, calls no Pipeline steps, and returns or stores only serializable values.
Does code inside node automatically execute all
Groovy logic on the agent?
No. Pipeline Groovy orchestration runs on the controller; agent processes are invoked by steps such as sh or bat.
What should an operator do with
program.dat?
Treat it as internal persisted Pipeline state: observe metadata if needed, but do not edit it as a troubleshooting or recovery technique.
What does restart durability not guarantee?
It does not make external systems transactional or atomic with Jenkins; side effects still need idempotency, target verification, or compensation.
Official references and version notes
- Jenkins LTS changelog — current Jenkins LTS and tested Java configurations.
-
Pipeline CPS Method Mismatches
— CPS transformation boundaries,
@NonCPS, constructors, closures, and mismatch diagnostics. -
Pipeline: Groovy plugin
— Pipeline execution engine, persistence model,
program.dat, rehydration, and@NonCPScontract. -
Pipeline Best Practices
— controller/agent boundaries, serializable state, and safe use of
@NonCPS. - Scaling Pipelines — durability modes, persistence trade-offs, controller CPU/memory, and restart implications.
- Script Security plugin — Groovy sandbox and script-approval security boundary.
- Jenkins Pipeline handbook — durable, pausable Pipeline fundamentals.
Rechecked on 2026-09-16. Examples assume Jenkins 2.568.3 LTS (tested with Java 21 and 25), Pipeline: Groovy 4380.v6eb_8378b_9647, Pipeline: Supporting APIs 1015.v785e5a_b_b_8b_22, and Script Security 1422.v06869826dd9b_. The mandatory path is local/disposable, uses synthetic state and fake identities, and requires no commercial service. Plugin releases are independent of Jenkins core, so record the versions actually installed on your controller before applying these lessons.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.