Groovy CPS, Serialization, @NonCPS, Pipeline State, Restarts, and Common Pipeline Programming Pitfalls: Diagnostics, Failure Modes, Security, and Performance
Diagnose CPS and serialization failures by preserving the original run, identifying the transformation boundary, distinguishing state/agent/external layers, and applying the smallest repair without weakening Script Security or durability.
Learning objectives
- Diagnose NotSerializableException from preserved stack/error evidence and nearby suspension points.
- Recognize and repair calls from @NonCPS/native contexts into CPS-transformed Pipeline code.
- Interpret CPS method-mismatch warnings caused by native methods receiving CPS closures.
- Identify controller memory/CPU pressure caused by large or complex Pipeline state.
- Keep sandbox/approval, restart, workspace, and external side-effect boundaries explicit during troubleshooting.
1. Evidence-first diagnostic ladder
Do not immediately rerun, lower durability, disable sandboxing, or rewrite the job. Preserve the build URL/number, source SHA, console exception, stage/step, node/workspace, and any external target state first.
- Preserve job/build/queue IDs and first-failure evidence.
- Confirm Jenkins core, Java, Pipeline: Groovy, Supporting APIs, and Script Security baseline.
- Confirm exact Jenkinsfile/library revision and build cause.
- Identify the most recent suspension/asynchronous step.
- List variables/closures that remain in scope across that point.
- Classify the boundary: CPS serialization, CPS/native mismatch, agent/workspace, security approval, or external system.
- Apply the smallest code repair at that boundary.
- Run a new build while preserving the broken run for comparison.
2. Failure A — NotSerializableException
def matcher = ('artifact-2026.09.16' =~ /artifact-(.+)/)
matcher.find()
echo "value=${matcher.group(1)}"
sleep 2
echo "again=${matcher.group(1)}"
Interpretation: the matcher is retained while Pipeline reaches a
suspension. The repair is not “add retry” and not “change
durability.” Extract the small value before suspension, ideally
inside a pure @NonCPS helper if native Groovy semantics
are needed.
3. Failure B — Pipeline step called from @NonCPS
@NonCPS
def badCompile() {
node('lab-linux') {
sh './build.sh'
}
}
badCompile()
This reverses the legal call direction. Jenkins may report a CPS
method mismatch, for example that it expected the helper but wound
up catching node. Repair by removing
@NonCPS if Pipeline steps are required, or keep the
native helper pure and call Pipeline steps in CPS code.
@NonCPS
String normalizedTarget(String raw) {
return raw.trim().toLowerCase()
}
def target = normalizedTarget(params.TARGET)
node('lab-linux') {
sh "./build-safe.sh '${target}'"
}
4. Failure C — CPS closure passed to a native method
def orderByLength(List<String> names) {
names.toSorted { left, right -> left.length() <=> right.length() }
}
def ordered = orderByLength(['bbb', 'a', 'cccc', 'dd'])
echo "ordered=${ordered}"
Some native Groovy methods are not CPS-transformed but receive a
CPS-transformed closure. The result can be incorrect and the log may
contain a method-mismatch warning. A safe repair is to encapsulate
the entire native operation inside @NonCPS and return a
normal serializable list:
@NonCPS
List<String> orderByLength(List<String> names) {
return names.toSorted { left, right -> left.length() <=> right.length() }
}
5. Failure D — oversized Pipeline state and controller pressure
Symptoms may include high controller heap, CPU, garbage collection, slow persistence, or unexpectedly large run-state files. A common cause is reading large files into Groovy variables, accumulating huge maps across branches, or generating thousands of tiny CPS steps.
Preserve controller metrics/thread evidence, then reduce Pipeline state: process data on agents, consolidate shell operations, archive large files, and retain compact summaries only.
6. Failure E — restart is mistaken for external atomicity
A deployment API call may have reached the target before the controller stopped. After resume, Jenkins program state can continue, but the external system may already be changed. Before repeating the operation, query the target using the immutable operation/release ID. Resume logic must distinguish “Jenkins did not record completion” from “the target did not change.”
7. Security failure — bypassing sandbox/approval to escape CPS pain
Do not disable Script Security, broadly approve signatures, or use Script Console as a Pipeline workaround. Unsandboxed Groovy can call internal Jenkins APIs and expand controller compromise impact. The safe response is to choose a supported Pipeline step, change the CPS/native boundary, or encapsulate complex behavior in a reviewed plugin/tool rather than weakening trust controls.
8. Performance evidence and causality
| Symptom | Likely layer | Evidence | Small repair |
|---|---|---|---|
| Serialization exception near sleep/input | CPS state | Exception + in-scope object | Extract serializable value |
| “expected to call … wound up catching …” | CPS/native mismatch | Warning + call site | Correct call direction / @NonCPS boundary |
| High controller heap | Pipeline state/computation | heap/GC + variable size | Agent-side processing |
| Workspace file missing after agent loss | Agent/workspace | node/workspace history | Artifact/stash strategy |
| Duplicate external action after restart | External side effect | target operation ID/state | Idempotency/verification |
9. Intentionally broken workflow and repair sequence
Use a dedicated build with the matcher-across-sleep example. Capture
the original build URL, source SHA, exception, and
program.dat metadata if present. Commit a repair that
returns the extracted string from a pure
@NonCPS helper. Run a new build and compare. Do not
delete the broken build or overwrite its evidence.
10. Troubleshooting shortcuts to reject
- Do not lower durability just to avoid a serialization exception.
- Do not disable the Groovy sandbox or bulk-approve signatures to make a script run.
- Do not move untrusted builds onto the controller.
-
Do not edit
program.dator internal build XML as a repair. - Do not blindly rerun an external mutation after restart.
- Do not keep giant report/object graphs in Pipeline variables.
Knowledge check
What is the first action after a NotSerializableException?
Preserve the original build/source/error evidence and identify objects still in scope around the nearest suspension point before changing code.
What does a warning like “expected to call X but wound up catching node” suggest?
A CPS/native method mismatch, commonly caused by non-CPS code calling a CPS-transformed Pipeline step.
How should you respond to large controller-side Pipeline state?
Move substantial computation/data processing to an agent or external tool and retain only a compact serializable summary.
Why is lowering durability not a valid serialization repair?
It changes persistence frequency and may mask when the defect appears, but the Pipeline still contains invalid state.
What must be checked before repeating a side effect after restart?
The external target’s state using an immutable operation/release identity, because Jenkins restart durability does not make the target transactional.
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.