Jenkins Scripted Pipeline and Groovy: Write Production-Grade CI/CD Without the Pain
Jenkins Scripted Pipeline and Groovy: production patterns, gotchas, and when to avoid it.
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
Use Scripted Pipeline when you need complex logic, dynamic stages, or error handling that Declarative Pipeline can't express. But beware: Groovy's runtime metaprogramming and Jenkins' serialization model will bite you if you don't understand CPS (Continuation Passing Style) transformation.
Think of Jenkins Scripted Pipeline as a recipe that can make decisions on the fly. A Declarative Pipeline is like a fixed menu — you pick from what's listed. Scripted is like a chef who can improvise based on what's in the fridge. But that improvisation comes with rules: you can't use fresh ingredients that don't survive freezing, because Jenkins freezes and thaws your recipe between steps.
You've seen it: a Jenkinsfile that looks like spaghetti, fails randomly, and nobody wants to touch it. Scripted Pipeline gets blamed, but the real culprit is ignorance of how Groovy and Jenkins' execution model actually work. I've debugged pipelines at 3am where a simple each loop caused a NotSerializableException because someone used a closure that captured a non-serializable reference. This article will teach you to write Scripted Pipelines that are robust, debuggable, and won't make you the on-call hero everyone resents.
Why Scripted Pipeline Exists: When Declarative Isn't Enough
Declarative Pipeline is great for 80% of use cases: build, test, deploy in a linear flow. But the moment you need conditional stage execution based on branch name, dynamic parallel branches, or error recovery that retries with backoff, Declarative's rigid structure fights you. Scripted Pipeline gives you if/else, for loops, try/catch/finally, and full Groovy power. The trade-off? You lose the automatic error handling and visual stage view that Declarative provides. I've seen teams try to shoehorn complex logic into Declarative using script blocks — that's just Scripted Pipeline with extra steps and no benefits. Use Scripted when you need it, not because you 'might' need it later.
def data = readJSON(...) outside a node block). Jenkins serializes the entire script state between stages. If data is huge, you'll get OOM or NotSerializableException. Always scope variables inside node blocks.The CPS Transformation: Why Your Groovy Code Behaves Differently
Jenkins doesn't run your Groovy code directly. It transforms it using Continuation Passing Style (CPS) to allow the pipeline to be paused and resumed (e.g., waiting for user input or a node). This transformation has consequences: closures can't capture non-serializable objects, loops have a limit (default 4096 iterations), and some Groovy idioms like each with method references break. The classic symptom: java.io.NotSerializableException on a closure that references this or a field. The fix: use @NonCPS annotation on methods that don't need to be resumed, or avoid capturing non-serializable references. I once spent two hours debugging a pipeline that failed only on the second run because a closure captured a File object that was created in a previous build.
Error Handling: Try/Catch/Finally Patterns That Actually Work
In production, pipelines fail. Network timeouts, flaky tests, insufficient disk space. Scripted Pipeline's try/catch/finally is your safety net. But there's a gotcha: if you catch an exception and don't rethrow, the stage is marked as SUCCESS even though it failed. Always rethrow or set build result explicitly. Another pattern: use retry with backoff for flaky steps. I've seen teams wrap a sh 'curl ...' in a retry(3) block without a timeout — the curl hung forever. Always combine retry with timeout. Here's a robust deployment step that retries with exponential backoff and fails fast if the cluster is down.
currentBuild.result = 'FAILURE', Jenkins marks the stage green. Your deployment could be broken but the pipeline shows SUCCESS. Always rethrow or explicitly fail.Parallel Execution: Don't Just Throw Stages at It
Parallel stages speed up builds, but naive parallelism causes resource contention. Jenkins' parallel step runs branches in separate threads, but they share the same node's workspace and executors. If you have 10 parallel branches each running sh 'mvn test', you'll thrash the CPU and disk. The pattern: limit parallelism using a lock resource or use separate agents. Another gotcha: variables defined inside parallel branches are not visible outside — you must use script to assign to a global variable. Here's a pattern for running integration tests in parallel with a max of 3 concurrent runs.
def index = i) because closures close over variables, not values. Without capture, all branches see the final value of i.Shared Libraries: Don't Repeat Yourself Across Pipelines
When you have multiple repos with similar pipelines, copy-paste is a maintenance nightmare. Jenkins Shared Libraries let you define reusable Groovy code in a separate repo and load it into any pipeline. The key: version your library with tags, and reference a specific version in your Jenkinsfile. Never use @Library('my-lib@master') — that's a moving target that will break your pipelines. I've seen a team's Friday deploy fail because someone pushed a breaking change to master. Use @Library('my-lib@v1.2.3') instead. Also, shared library code runs outside the CPS sandbox by default, so you can use full Groovy — but you must mark methods that call pipeline steps with @NonCPS or use steps object.
@master means any push to master immediately affects all pipelines using that library. Pin to @v1.2.3 and update deliberately.When Not to Use Scripted Pipeline
Scripted Pipeline is powerful, but it's not always the right tool. If your pipeline is a simple linear sequence of build, test, deploy with no conditionals, use Declarative. It's easier to read, has better error messages, and the Blue Ocean UI renders stages better. Also, if your team is not comfortable with Groovy, Scripted will be a source of bugs. I've seen teams write 500-line Scripted pipelines that could be 50 lines of Declarative with a few script blocks. Another case: if you need to integrate with external systems via REST APIs, consider using a dedicated tool like Jenkins Configuration as Code or a separate orchestration service. Scripted Pipeline is not a general-purpose programming environment — it's a CI/CD DSL. Don't try to build a microservice in it.
script block for the complex logic. This gives you the best of both worlds: Declarative's structure and Scripted's power where needed. Most pipelines only need 1-2 script blocks.The 4GB Heap That Wasn't Enough
OutOfMemoryError: Java heap space on a simple readFile step.readFile returns a String, which is fine. But the pipeline also loaded a huge JSON config via readJSON and stored it in a global variable. Jenkins serializes the entire script state between stages, including that variable, causing heap bloat. The readJSON result was a Map with deep nesting, and serialization created a massive object graph.node blocks and scope variables locally. Use @NonCPS on methods that process data to avoid serialization of intermediate state. Set -Xmx2g for the Jenkins agent, but more importantly, refactor to not hold large objects across stage boundaries.- Anything you assign to a global variable in a pipeline gets serialized between stages.
- Keep state small and local.
java.io.NotSerializableExceptionthis, owner, or custom classes). 2. Mark the enclosing method with @NonCPS if it doesn't call pipeline steps. 3. Or refactor to avoid capturing the object (e.g., pass as parameter).input step without timeout. 2. Check if a node block is waiting for an executor — verify agent availability. 3. Check for infinite loops in Groovy code — add echo statements to trace execution.java.lang.StackOverflowError in pipeline@NonCPS. 3. Or rewrite iteratively.Key takeaways
@NonCPS for pure computations.retry with timeout to avoid hanging pipelines. Never catch exceptions without rethrowing or setting build result.script blocks for complex logicInterview Questions on This Topic
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
That's Jenkins. Mark it forged?
3 min read · try the examples if you haven't