Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Jenkins By James Joyner IV · · 10 min read

Jenkins Error: 'com.cloudbees.groovy.cps.impl.CpsCallableInvocation ... Not serializable' — Cause, Fix, and Troubleshooting Guide

Quick answer

Fix Jenkins Pipeline CPS 'CpsCallableInvocation ... Not serializable': learn why state is serialized across steps, and use @NonCPS and restructuring to fix it.

  • #jenkins
  • #ci-cd
  • #troubleshooting
  • #errors
Free toolkit

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

Jenkins Pipeline runs your Groovy through a Continuation-Passing-Style (CPS) interpreter so it can pause at any step, serialize the entire program state to disk, and resume later — even after a controller restart. When the interpreter reaches a step boundary and finds a local variable it cannot serialize, the build fails with a message like:

an exception which occurred:
	in field groovy.lang.Closure.delegate
	in object com.cloudbees.groovy.cps.impl.CpsCallableInvocation@1a2b3c
Caused: java.io.NotSerializableException: java.util.regex.Matcher
	... com.cloudbees.groovy.cps.impl.CpsCallableInvocation ...
Not serializable

The CpsCallableInvocation in the trace is CPS trying to capture the continuation across a step. The actual offender is named further down (here, a java.util.regex.Matcher) — a non-serializable object being held in a local variable across a sh, sleep, input, or any other Pipeline step.

Symptoms

  • The build fails at a Pipeline step (sh, sleep, input, stage, parallel) rather than at plain Groovy.
  • The trace mentions com.cloudbees.groovy.cps.impl.CpsCallableInvocation and a NotSerializableException naming a concrete type (Matcher, Pattern, JsonSlurperClassic result, an SSHClient, a JDBC Connection, etc.).
  • The offending type is often the result of a regex match, a JSON/XML parse, or a live handle (socket, file, DB connection).
  • Moving code that “worked in a script” into a Pipeline steps block suddenly triggers it.
  • The failure is reproducible and deterministic, unlike environmental flakes.

Common Root Causes

  • A non-serializable local held across a step — you assign something like def m = (log =~ /error/) (a Matcher) and then call sh/sleep; CPS must serialize m at that boundary and cannot.
  • Parsing with a non-CPS-friendly typenew JsonSlurper().parseText(...) returns a LazyMap/non-serializable object; using readJSON/JsonSlurperClassic and keeping the raw parser objects around causes it.
  • Live resource handles kept across steps — database connections, HTTP clients, or file streams opened in one step and used after another step cannot be persisted.
  • Iterators/streams across a stepIterator, Stream, and some collection views are not serializable; iterating them with a step inside the loop body triggers it.
  • Closures capturing non-serializable delegates — a closure that closes over a non-serializable object gets dragged into the continuation.
  • Method that should be @NonCPS returning a CPS-transformed object — mixing CPS and non-CPS incorrectly leaves an un-persistable invocation on the stack.

How to diagnose

Read past the CpsCallableInvocation frames to the Caused: java.io.NotSerializableException: <Type> line — that concrete type is the object you must stop holding across a step.

Identify the step boundary. The failure occurs at the first Pipeline step after the non-serializable local is created. In the console output, find the last successful line before the exception; the offending assignment is just above it in the Jenkinsfile.

Reproduce and inspect quickly in Manage Jenkins → Script Console (this runs plain Groovy, not CPS, so use it only to confirm which types are non-serializable):

// Which types are (not) serializable?
println( (java.io.Serializable).isAssignableFrom(java.util.regex.Matcher) )  // false
println( (java.io.Serializable).isAssignableFrom(String) )                   // true

Turn on Pipeline durability/step detail if you need the exact step: Manage Jenkins → System → “Pipeline Speed/Durability Settings” shows the durability mode. In Blue Ocean or the classic Pipeline Steps view, the failing step is highlighted red — open it to see the frame.

If you cannot tell which local is the problem, bisect: temporarily replace the suspect assignment with a serializable primitive (e.g. capture .group() as a String) and see if the failure moves.

Fixes

1. Don’t hold the non-serializable value across a step

Extract the primitive you actually need before the next step, so no Matcher/parser object survives:

// BAD: Matcher is held across sh()
def m = (buildLog =~ /version=(\d+)/)
sh 'sleep 1'
echo m[0][1]            // fails: Matcher not serializable across the step

// GOOD: pull the String out immediately, discard the Matcher
def version = (buildLog =~ /version=(\d+)/) ? (buildLog =~ /version=(\d+)/)[0][1] : ''
sh 'sleep 1'
echo version            // version is a String — serializable

2. Isolate regex/parsing logic in an @NonCPS method

@NonCPS methods run outside the CPS transform, so local non-serializable objects live and die inside the method and never cross a step. The method must return a serializable value and must not call Pipeline steps:

@NonCPS
String extractVersion(String log) {
    def m = (log =~ /version=(\d+)/)   // Matcher lives only inside this method
    return m ? m[0][1] : ''            // returns a plain String
}

node {
    def v = extractVersion(currentBuild.rawBuild.log)
    sh "echo building ${v}"            // v is serializable — safe across the step
}

Do not put sh, checkout, input, etc. inside an @NonCPS method — step calls there behave unpredictably.

3. Use Pipeline-native parsing steps

Prefer the Pipeline Utility Steps that return plain, serializable maps/lists instead of raw parser objects:

def data = readJSON text: sh(script: 'cat config.json', returnStdout: true)
def cfg  = readYaml file: 'settings.yaml'
// data / cfg are serializable Maps/Lists — safe to keep across steps

If you must use JsonSlurper, wrap it in an @NonCPS method and return String/Map of primitives.

4. Open and close live handles inside a single non-step scope

Never keep a DB connection, HTTP client, or stream open across a step. Do the whole interaction in one @NonCPS (or one uninterrupted block) and return only the result:

@NonCPS
List<String> queryUsers(String jdbcUrl) {
    def out = []
    def conn = java.sql.DriverManager.getConnection(jdbcUrl)  // handle stays local
    try {
        def rs = conn.createStatement().executeQuery('SELECT name FROM users')
        while (rs.next()) out << rs.getString('name')         // copy into a List<String>
    } finally {
        conn.close()
    }
    return out            // only serializable data escapes
}

5. Convert iterators/views to serializable collections

Replace Java Iterator/Stream usage that spans a step with a materialized list, and use classic for/indexed loops:

def files = sh(script: 'ls *.jar', returnStdout: true).trim().split('\n') as List
for (int i = 0; i < files.size(); i++) {
    sh "sha256sum ${files[i]}"     // list of Strings, indexed loop — CPS-safe
}

What to watch out for

  • This is different from a plain NotSerializableException on a persisted global or object graph — here the trigger is specifically a CPS continuation captured at a step boundary. If your trace has no CpsCallableInvocation frame, see the NotSerializableException sibling guide instead.
  • @NonCPS is a scalpel, not a blanket fix — a @NonCPS method that calls a Pipeline step or returns a non-serializable object creates new, subtler failures.
  • Higher durability modes (MAX_SURVIVABILITY) serialize more aggressively and surface these bugs that PERFORMANCE_OPTIMIZED might mask; test in the mode you run in production.
  • Groovy’s =~ returns a Matcher and ==~ returns a boolean — prefer ==~ for pure match tests to avoid ever creating a Matcher.
  • Loading a Shared Library and holding one of its non-serializable objects across a step triggers the same error — apply the same @NonCPS/extract-primitive discipline in library code.
Free download · 368-page PDF

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.