Jenkins Error: 'java.io.NotSerializableException' — Cause, Fix, and Troubleshooting Guide
Fix Jenkins pipeline 'java.io.NotSerializableException': stop holding matchers, LazyMap, or streams across steps in CPS Groovy using @NonCPS and local scope.
- #jenkins
- #ci-cd
- #troubleshooting
- #errors
Fixing errors like this? Get 500 free DevOps AI prompts
500 copy-paste AI prompts for the stack you actually run — one PDF, free.
Overview
A Jenkins Pipeline failed because the CPS (Continuation-Passing Style) engine tried to serialize the running program’s state and hit an object that cannot be serialized. Pipelines are durable: at every step boundary (sh, checkpoint, input, stash, an agent hop) Jenkins saves the entire program — every local variable currently in scope — to disk so a build can survive a controller restart. If a variable holds something non-serializable at that moment, you get:
java.io.NotSerializableException: java.util.regex.Matcher
at org.jenkinsci.plugins.workflow.support.pickles.serialization.RiverWriter...
at ...
Caused: an attempt to serialize the build's program state failed
The exception names the offending class — java.util.regex.Matcher, groovy.json.internal.LazyMap, java.io.InputStream, and so on. These are objects that must not be alive across a step. This guide is about that step-boundary serialization of local objects; for failures rooted in global variables and shared-library binding, see the Groovy CPS not serializable guide, which covers that adjacent case.
Symptoms
- A pipeline fails right after (or during) a
sh,stash,input, orcheckpointstep withNotSerializableException. - The message names a specific class:
java.util.regex.Matcher,groovy.json.internal.LazyMap,java.io.InputStream,java.io.File, a JDBC connection, etc. - The same code runs fine in the Groovy console or a plain script but fails in a Jenkins Pipeline.
- Moving the offending logic to before the first
shstep (or wrapping it) makes the error disappear. - The failure is deterministic — same line, same class, every run.
Common Root Causes
- A regex Matcher held across a step —
def m = text =~ /.../creates ajava.util.regex.Matcher, which is not serializable; ifmis still in scope at the next step, serialization fails. - JsonSlurper
LazyMap/LazyListretained across a step —readJSON/new JsonSlurper().parseText(...)returns lazy, non-serializable collections that must not survive a step boundary. - An open stream, reader, or file handle in scope —
InputStream,Reader, sockets, and JDBC connections are inherently non-serializable and cannot be carried across steps. - Objects from
@NonCPS-only libraries leaking into CPS scope — a helper returns a non-serializable object and the caller stores it in a CPS-visible local. - Wide variable scope — declaring a heavy/temporary object at pipeline or stage scope so it stays alive far longer than the few lines that use it.
- Non-serializable field on a shared-library class — a
Serializablestep class holds a transient-worthy field (a client, a matcher) that is not markedtransient.
How to diagnose
Read the class name in the exception — it points straight at the offending object type. Then find where that type is created and whether a step runs while it is still in scope.
Reproduce with a minimal Pipeline to confirm the pattern (a Matcher held across sh):
node {
def m = ('build-42' =~ /build-(\d+)/) // java.util.regex.Matcher
sh 'echo crossing a step boundary' // <-- serialization happens here
echo m[0][1] // m still in scope => NotSerializableException
}
Check the full stack trace in the build’s Console Output and, for CPS internals, the pipeline’s Replay view to iterate quickly. In the Script Console you can confirm which classes are serializable:
// Quick serializability check (Script Console)
[java.util.regex.Matcher, groovy.json.internal.LazyMap, java.util.HashMap].each { c ->
println "${c.name}\tSerializable=${Serializable.isAssignableFrom(c)}"
}
Search your shared library and Jenkinsfiles for the usual offenders:
grep -RnE '=~|new JsonSlurper|readJSON|new File\(|\.newInputStream|getInputStream' vars/ src/ Jenkinsfile
Fixes
1. Confine a Matcher to a @NonCPS method
Never let a Matcher live across a step. Do the match, extract the plain String/value you need, and drop the Matcher — ideally inside a @NonCPS helper so the CPS transformer never tracks it:
@NonCPS
def extractBuildNumber(String text) {
def m = (text =~ /build-(\d+)/)
return m ? m[0][1] : null // Matcher is local to this method; only a String escapes
}
node {
def n = extractBuildNumber('build-42')
sh 'echo crossing a step boundary'
echo "build number: ${n}" // n is a String — safe to serialize
}
A @NonCPS method runs to completion without CPS instrumentation, so objects created inside it are never candidates for cross-step serialization — but you must not call pipeline steps (sh, echo) from inside a @NonCPS method.
2. Convert lazy JSON into plain, serializable collections immediately
readJSON/JsonSlurper returns lazy maps and lists. Deep-copy them into ordinary HashMap/ArrayList before any step, or parse inside a @NonCPS method and return primitives:
@NonCPS
Map parseConfig(String json) {
def lazy = new groovy.json.JsonSlurper().parseText(json)
// Materialize into a plain, serializable structure
return new HashMap(lazy as Map)
}
node {
def cfg = parseConfig(readFile('config.json'))
sh "deploy --env ${cfg.environment}" // cfg is a plain HashMap — serializes fine
}
If you use the Pipeline Utility Steps readJSON, wrap the result the same way, or read only the fields you need into String/number locals.
3. Scope temporary objects tightly and null them before a step
Keep non-serializable temporaries inside the smallest block that uses them so they are out of scope before the next step. Do not declare them at pipeline/stage level:
node {
String firstLine
// stream is used and closed inside this block; never crosses a step
new File(pwd(), 'report.txt').withReader { r -> firstLine = r.readLine() }
sh 'echo now safe to cross a step'
echo firstLine
}
Prefer pipeline steps over raw Java I/O where possible — readFile/writeFile return String, which is serializable, instead of a live handle.
4. Mark non-serializable fields transient in shared-library classes
If a shared-library class must implement Serializable (so it can be a step or field), mark any heavy or non-serializable field transient and lazily rebuild it:
class ApiClient implements Serializable {
private final String baseUrl
private transient HttpURLConnection conn // not serialized; rebuilt on demand
ApiClient(String baseUrl) { this.baseUrl = baseUrl }
}
5. Do heavy processing on the agent, not in pipeline scope
If you are parsing large output, push it to a subprocess and read back only the small result, which sidesteps CPS serialization entirely:
node {
sh 'grep -c ERROR build.log > count.txt'
int errors = readFile('count.txt').trim().toInteger()
echo "errors: ${errors}"
}
What to watch out for
- Any object created by
=~is aMatcher— it is the single most common cause. Use==~for a boolean match, or extract the value in a@NonCPSmethod. @NonCPSmethods must never call pipeline steps (sh,echo,node). They run outside CPS, so steps are unavailable and behavior is undefined.- Distinguish this from the global-variable/binding case — if the class named is your own script or a shared global, read the Groovy CPS not serializable guide instead.
- The error only fires when a step boundary is crossed while the object is in scope; code that never hits a step can look deceptively fine until you add a
sh. readFile/writeFile/readJSONresults are safer than rawjava.io.*— prefer pipeline steps, and materialize lazy structures early.- The Jenkins guides hub has more pipeline-scripting fixes.
Related
- Jenkins Error: Groovy CPS not serializable — the adjacent case, focused on global variables and shared-library binding rather than local objects across a step.
- Jenkins Error: scripts not permitted to use method — the script-security sandbox blocking
@NonCPShelper methods. - Jenkins Error: groovy.lang.MissingPropertyException: No such property — another common Jenkinsfile Groovy scripting failure.
More Jenkins prompts & error guides
Every Jenkins AI prompt and troubleshooting guide, in one place.
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.