Back to Blog
Lesson 54 of the CI/CD: Continuous Integration from Scratch course
DevOpsAugust 29, 20264 min read

Pipeline Resilience: Configuring Retries, Timeouts, and Error Handling

Learn how to build resilient CI/CD pipelines in GitHub Actions. Master retries, timeouts, and error handling to keep your automation reliable and stable.

DevOpsGitHub ActionsCI/CDReliabilityResilience
Close-up of a computer screen displaying an authentication failed message.

Previously in this course, we explored automating releases to standardize your versioning and delivery processes. Now that you have a functioning delivery mechanism, we need to ensure it doesn't break due to the inevitable "hiccups" of the internet: network timeouts, API rate limits, or temporary service outages.

In distributed systems, we often talk about designing for failure because the assumption of a "perfect connection" is a recipe for manual intervention. When your pipeline is the backbone of your delivery, you need it to be self-healing.

The Anatomy of Pipeline Resilience

A resilient pipeline treats transient errors—errors that are likely to resolve if you just try again—as a normal part of life. We handle these using three primary mechanisms:

  1. Retries: Automatically re-running a step that failed due to a temporary network issue.
  2. Timeouts: Preventing a "zombie" job from consuming your billable minutes indefinitely.
  3. Graceful Error Handling: Using conditional logic to perform cleanup or notification tasks only when a previous step fails.

Implementing Retries in GitHub Actions

Not every failure deserves a retry. If your code is syntactically invalid, a retry won't fix it. However, if you are calling an external API or pulling a container image from a registry, a 5-second network lag could fail your entire build.

In GitHub Actions, you can set a retry strategy at the job level or use third-party composite actions for granular step retries. For a standard job, the most effective way to build resilience is to ensure your run commands are idempotent (meaning they can be run multiple times without causing side effects).

YAML
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to Server
        # Using a simple shell loop for retry logic
        run: |
          for i in {1..3}; do
            ./deploy_script.sh && break || sleep 5
          done

Configuring Step Timeouts

A common pitfall is a pipeline that hangs forever because a subprocess didn't exit. By default, GitHub Actions has a 6-hour timeout for jobs, but that is rarely what you want for individual steps. Always define a timeout-minutes for steps that interact with external services.

YAML
    steps:
      - name: Run Integration Tests
        timeout-minutes: 10
        run: ./run_integration_tests.sh

If the tests exceed 10 minutes, the action will be forcefully terminated, allowing you to catch the failure quickly rather than waiting hours for the runner to timeout.

Handling Transient Errors: A Worked Example

Let’s refine our deployment job. We want to ensure that if the connection to our production server fails, we attempt the connection three times before failing the build.

YAML
jobs:
  production-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
      
      - name: Deploy with Retry
        timeout-minutes: 5
        run: |
          attempt=1
          until ./deploy.sh || [ $attempt -eq 3 ]; do
            echo "Attempt $attempt failed. Retrying in 10 seconds..."
            attempt=$(( attempt + 1 ))
            sleep 10
          done
          ./deploy.sh # Final attempt

Hands-on Exercise

Modify your current deployment workflow (or create a new test workflow):

  1. Add a timeout-minutes: 2 to your primary build step.
  2. Introduce a fake "flaky" command that fails 50% of the time (e.g., [ $((RANDOM % 2)) -eq 0 ] && exit 1).
  3. Implement a loop-based retry mechanism to ensure the command eventually succeeds if the first attempt fails.
  4. Verify the output in the GitHub Actions UI to see your retry logic in action.

Common Pitfalls

  • Retrying Non-Transient Errors: Never retry code compilation or linting failures. These are deterministic; if they fail once, they will fail again. Only retry network-bound or external service operations.
  • Infinite Loops: When writing custom retry loops in Bash, always include a counter or a hard exit condition to prevent the pipeline from running until the global timeout is reached.
  • Ignoring Logs: When using retries, your logs can get noisy. Ensure your retry script logs clear messages so you can distinguish between a "fixed on retry" and a "failed immediately" event.

FAQ

Q: Should I use continue-on-error for resilience? A: Use continue-on-error: true only if you want the pipeline to proceed even if a step fails. This is useful for gathering diagnostic data, but dangerous for deployment steps where you need the pipeline to stop if a failure occurs.

Q: Does GitHub provide built-in retry actions? A: Yes, the marketplace has many retry actions, but learning to write simple shell-based retries makes your pipelines more portable and easier to audit.

Recap

Building pipeline resilience is about anticipating failure. By combining timeout-minutes for guardrails and simple retry loops for transient network issues, you transform a fragile automation process into a robust, self-recovering system. This approach aligns with the principles of writing post-mortems by ensuring your infrastructure is built to handle the unexpected.

Up next: We will look at how to maintain high standards of quality even when your infrastructure grows, starting with testing your Infrastructure as Code (IaC) scripts.

Similar Posts