Skip to main content

Command Palette

Search for a command to run...

Stop GitHub Actions Running Forever

Learn: Stop GitHub Actions Running Forever

Updated
6 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Stop GitHub Actions Running Forever: 2026 Troubleshooting Guide

Problem

Your GitHub Actions workflow is stuck. The job shows "in progress" for hours. Logs stopped updating 30 minutes ago. You're burning through your monthly action minutes. The runner is unresponsive. You can't cancel it. Welcome to the infinite loop nightmare that plagues CI/CD pipelines in 2026.

This isn't theoretical—it's happening across thousands of repositories right now. Workflows hang silently. Runners ghost. Resources drain. Teams lose productivity. And the worst part? GitHub's UI sometimes won't even let you kill the job cleanly.


Why This Happens (2026 Context)

1. AI-Generated Workflow Complexity

By 2026, many teams use AI code generators (GitHub Copilot, Claude, etc.) to scaffold workflows. These tools often create overly complex dependency chains, nested conditionals, and implicit waits that don't fail gracefully. A single missing timeout cascades into a hung workflow.

2. Containerization at Scale

Docker image pulls, layer caching, and registry timeouts are now common culprits. With millions of developers pulling from DockerHub simultaneously, transient network failures leave workflows in zombie states. The container never fully loads, but the job never fails either.

3. Distributed Test Suites

2026 workflows orchestrate tests across 50+ parallel jobs. One flaky test service (database, API mock, message queue) hangs indefinitely. The workflow waits for all jobs to complete. One zombie job = entire pipeline frozen.

4. Third-Party Action Bloat

The GitHub Marketplace now hosts 100,000+ actions. Many are unmaintained. Actions that worked in 2024 now hang due to deprecated APIs, changed authentication, or silent failures in their underlying dependencies.

5. Resource Exhaustion on Runners

Self-hosted runners in 2026 often run on shared infrastructure. A memory leak in one job consumes all RAM. The runner becomes unresponsive. New jobs queue indefinitely. GitHub's cloud runners have better isolation, but cost more.

6. Implicit Waits in Deployment Workflows

Workflows that deploy to Kubernetes, serverless platforms, or cloud services often wait for health checks. If the health check endpoint is misconfigured or the service never becomes healthy, the job hangs forever.


Solution (Practical Examples)

1. Add Explicit Timeouts Everywhere

name: CI Pipeline

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 15  # Job-level timeout

    steps:
      - uses: actions/checkout@v4
        timeout-minutes: 5  # Step-level timeout

      - name: Build Application
        timeout-minutes: 10
        run: |
          npm install
          npm run build

      - name: Run Tests
        timeout-minutes: 20
        run: npm test

Why this works: Timeouts are your circuit breaker. Without them, a hung process runs forever. Set timeouts at both job and step levels. Be conservative—it's better to fail fast and retry than to waste 60 minutes.


2. Implement Health Checks for External Services

  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Kubernetes
        run: kubectl apply -f deployment.yaml

      - name: Wait for Rollout (with timeout)
        timeout-minutes: 10
        run: |
          kubectl rollout status deployment/my-app \
            --timeout=5m || exit 1

      - name: Health Check with Retry
        timeout-minutes: 5
        run: |
          for i in {1..30}; do
            if curl -f http://localhost:8080/health; then
              echo "Service healthy"
              exit 0
            fi
            echo "Attempt $i/30 - waiting..."
            sleep 10
          done
          echo "Health check failed"
          exit 1

Why this works: Explicit health checks prevent silent failures. The --timeout flag on kubectl rollout prevents infinite waits. The retry loop with explicit exit codes ensures the job fails cleanly if the service doesn't become healthy.


3. Cancel Dependent Jobs on Failure

name: Coordinated Pipeline

on: [push]

jobs:
  lint:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint

  build:
    needs: lint
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - run: npm run build

  test:
    needs: build
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
      - run: npm test

  deploy:
    needs: test
    if: success()  # Only run if all previous jobs succeeded
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh

Why this works: The needs keyword creates explicit dependencies. The if: success() condition prevents downstream jobs from running if upstream jobs fail. This stops cascading hangs.


4. Use Concurrency to Cancel Previous Runs

name: Smart Cancellation

on:
  push:
    branches: [main, develop]
  pull_request:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - run: npm run build

Why this works: If you push twice to the same branch, the first workflow is automatically cancelled. This prevents resource waste and ensures only the latest code is tested. Critical for high-velocity teams.


5. Monitor and Kill Hung Runners

name: Runner Health Check

on:
  schedule:
    - cron: '*/15 * * * *'  # Every 15 minutes

jobs:
  check-runners:
    runs-on: ubuntu-latest
    steps:
      - name: List Running Jobs
        run: |
          gh run list --status in_progress --limit 100 \
            --json databaseId,name,createdAt,status \
            --jq '.[] | select(.createdAt < now - 3600) | .databaseId' \
            > hung_jobs.txt

      - name: Cancel Hung Jobs
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          while read job_id; do
            echo "Cancelling job $job_id"
            gh run cancel $job_id
          done < hung_jobs.txt

Why this works: This scheduled workflow finds jobs running for more than 1 hour and cancels them. Prevents zombie jobs from consuming resources indefinitely. Adjust the threshold based on your typical job duration.


6. Validate Third-Party Actions

  validate-actions:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Audit GitHub Actions
        run: |
          # Check for unmaintained actions
          grep -r "uses:" .github/workflows/ | \
            grep -v "@v[0-9]" | \
            grep -v "actions/" && \
            echo "WARNING: Found unpinned or third-party actions" || true

      - name: Test Action with Timeout
        timeout-minutes: 5
        run: |
          # Test problematic action in isolation
          docker run --rm --timeout 300 \
            -e INPUT_TOKEN=${{ secrets.GITHUB_TOKEN }} \
            my-custom-action:latest

Why this works: Pinning action versions prevents silent breakage. Testing actions in isolation with timeouts catches hangs before they affect your main pipeline.


Prevention

Checklist for 2026 Workflows

  • Set timeouts at job AND step levels (never rely on defaults)
  • Use explicit health checks for any external service
  • Pin action versions to specific releases (e.g., @v4, not @main)
  • Implement concurrency controls to cancel redundant runs
  • Add retry logic with exponential backoff for flaky operations
  • Monitor runner health with scheduled cleanup jobs
  • Use if: success() conditions to prevent cascading failures
  • Test workflows locally with act before pushing
  • Log aggressively so you can debug hangs post-mortem
  • Set up alerts for jobs exceeding expected duration

Infrastructure Recommendations

  • Use GitHub-hosted runners for critical workflows (better isolation, auto-cleanup)
  • For self-hosted runners: implement memory limits, disk cleanup, and process monitoring
  • Use container registries with SLAs (not free DockerHub for production)
  • Implement network timeouts at the OS level on runners

Takeaway

GitHub Actions hanging forever is a symptom of missing guardrails. In 2026, with AI-generated workflows, complex dependencies, and distributed systems, explicit timeouts and health checks aren't optional—they're essential.

The golden rule: Every wait should have a timeout. Every external dependency should have a health check. Every job should be cancellable.

Start with timeouts. Add health checks. Monitor your runners. Your CI/CD pipeline will thank you.