Declarative Best Practices: Timeouts, Retries, Post
Wrap flaky work in retries, bound every pipeline with a timeout, and always clean up in post so a stuck build never holds an agent hostage.
The difference between a hobby pipeline and a production one is usually not the happy path — it is how it behaves when things go wrong. Three directives do most of the heavy lifting: timeout, retry, and post.
Bound everything with a timeout
A network hang or a wedged test runner can block an executor indefinitely. Set a top-level options { timeout(...) } so a build gives up instead of squatting on an agent forever. Add tighter stage-level timeouts around anything that talks to the network.
Retry only what is safe to retry
Retrying a flaky npm ci or a Docker pull is reasonable — it is idempotent. Retrying a deploy that already half-applied is dangerous. Use retry(n) narrowly, and combine it with timeout so a retry loop cannot run forever. The waitUntil and retry combo is handy for polling readiness.
Always clean up in post
The post block runs regardless of outcome. Put cleanWs(), artifact archiving, and test-report publishing in always; put alerts in failure and unstable. This is what keeps a red build from leaving a dirty workspace that breaks the next one.
Example
pipeline {
agent any
options {
timeout(time: 45, unit: 'MINUTES') // hard ceiling for the whole run
buildDiscarder(logRotator(numToKeepStr: '30'))
disableConcurrentBuilds()
}
stages {
stage('Install') {
steps {
// network step: bound it AND retry transient failures
retry(3) {
timeout(time: 5, unit: 'MINUTES') {
sh 'npm ci'
}
}
}
}
stage('Test') {
steps { sh 'npm test -- --ci' }
}
}
post {
always {
junit testResults: 'reports/*.xml', allowEmptyResults: true
cleanWs()
}
failure {
echo 'Build failed — see notifications lesson to wire up alerts.'
}
}
}When to use it
- A pipeline wraps a flaky HTTP health-check step in retry(3) so a single transient timeout does not fail the entire build.
- A timeout(time: 45, unit: 'MINUTES') option at the pipeline level prevents a hung integration test from holding an agent overnight.
- A post { always { cleanWs() } } block guarantees the workspace is wiped even when an earlier stage throws an exception.
More examples
Pipeline-level timeout and retry
Combines a 45-minute timeout, 2 retries on failure, log rotation, and guaranteed workspace cleanup.
pipeline {
agent any
options {
timeout(time: 45, unit: 'MINUTES')
retry(2)
buildDiscarder(logRotator(numToKeepStr: '30'))
}
stages {
stage('Build') { steps { sh 'mvn package' } }
}
post {
always { cleanWs() }
}
}Stage-level retry for flaky step
Retries a flaky integration test stage up to 3 times and always publishes results regardless of outcome.
stage('Integration Tests') {
options { retry(3) }
steps {
sh './run-integration.sh'
}
post {
always { junit 'reports/**/*.xml' }
}
}Post block with all conditions
Uses all five meaningful post conditions to handle cleanup, archiving, and state-change notifications.
post {
always { cleanWs() }
success { archiveArtifacts 'dist/**' }
failure { slackSend channel: '#builds', color: 'danger',
message: "FAILED: ${JOB_NAME}" }
fixed { slackSend channel: '#builds', color: 'good',
message: "RECOVERED: ${JOB_NAME}" }
unstable { echo 'Tests unstable — check results' }
}
Discussion