All posts

Find the Critical Path in Your GitHub Actions Workflow

Most jobs in a workflow can get faster without the run getting any faster. Learn to find the chain of jobs that sets your wall time, and what to do about it.

github-actionsci-performance

Your pull request workflow takes 16 minutes. You spend an afternoon making the lint job twice as fast. The workflow still takes 16 minutes.

This happens all the time, and it’s not bad luck. Once a workflow has more than one job, only some of those jobs decide how long you wait. The rest finish early and sit idle. This post shows how to find the jobs that matter, so the next afternoon you spend on CI speed actually shows up on the clock.

Contents

Jobs run in parallel, until needs says otherwise

GitHub Actions starts every job in a workflow at the same time, as long as there are runners free. The needs keyword is what turns that flat list into a graph: a job with needs: build waits until build has finished successfully.

jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [...]
  build:
    runs-on: ubuntu-latest
    steps: [...]
  e2e-tests:
    needs: build
    runs-on: ubuntu-latest
    steps: [...]
  deploy-preview:
    needs: [lint, e2e-tests]
    runs-on: ubuntu-latest
    steps: [...]

Here lint and build start together. e2e-tests waits for build, and deploy-preview waits for both lint and e2e-tests. The run is done when the last job is done.

The critical path sets the wall time

Follow every chain of needs from the first job to the last and add up the durations. The longest chain is the critical path, and its length is the shortest your run can possibly be. In the example, build (5 min) → e2e-tests (9 min) → deploy-preview (2 min) adds up to 16 minutes, while lint is done after 2.

Every job that isn’t on the critical path has slack: time it could grow without delaying anything. lint above has 12 minutes of slack. Make it faster and you get nothing back; make it 10 minutes slower and nobody notices either.

Try it on a slightly bigger workflow. Click a job to make it one minute faster and watch which clicks move the wall time.

Make the workflow faster

Wall time
16 min
Job minutes shaved
0
Run time change
0 min
lint
typecheck
unit-tests
build
e2e-tests
docker-image
deploy-preview
024681012141618
Minutes
Click a job to make it one minute faster. Orange jobs are on the critical path; the dashed outline is slack, time a job can grow without delaying the run.

A few things you’ll notice:

  • Shaving minutes off lint, typecheck or unit-tests doesn’t change the wall time at all. They have slack.
  • Every minute off build or e2e-tests comes straight off the run, until the critical path moves. At some point unit-tests becomes the longest chain and turns orange, and from then on that’s the job to work on.
  • Turning on the lint gate adds two minutes to every run, even though no job got slower.

The lint gate trap

That last point deserves its own section, because it’s one of the most common patterns in real workflows. The idea is sensible: lint is cheap and fails often, so make everything else needs: lint and save runner minutes when it fails.

What it costs is less obvious. The gate puts lint on the critical path of every run, including the large majority of runs where lint passes. You trade a few saved runner minutes on failing runs for a slower feedback loop on every run.

There are better ways to fail fast:

  • Run lint in parallel, and let it cancel the whole run when it fails. That saves most of the runner minutes without making passing runs wait:
lint:
  runs-on: ubuntu-latest
  permissions:
    contents: read
    actions: write
  steps:
    - uses: actions/checkout@v5
    - run: npm ci && npm run lint
    - if: failure()
      run: gh run cancel ${{ github.run_id }}
      env:
        GH_TOKEN: ${{ github.token }}
  • Run lint as a pre-commit hook or in the editor, so it rarely fails in CI at all.
  • If you do gate, gate only the expensive jobs (like a long end-to-end suite), not the whole graph.

How to find your critical path

GitHub doesn’t highlight the critical path for you, but the data is there. For a single run:

  1. Open the run and look at the workflow graph in the summary. It shows the needs edges.
  2. Note each job’s start and end times from the job list, or fetch them from the API with gh run view <run-id> --json jobs.
  3. Walk backwards from the last job to finish. At each job, the job it waited for longest is the next one on the path.

One run isn’t enough, though. Job durations vary from run to run: a cache miss here, a slow runner there. A job with only a minute of slack will land on the critical path in some runs and not in others. Look at the median and the 95th percentile duration of each job over a couple of weeks, and treat jobs with little slack as critical too.

Don’t forget queue time. A job that waits three minutes for a runner is three minutes longer as far as the critical path is concerned, even if its steps are fast. Larger runners and self-hosted pools often queue longer than the standard hosted ones.

Shortening the critical path

Once you know which jobs are on the path, you have three kinds of moves:

  • Make a critical job faster. Caching, a bigger runner, or less work in the job. This is where speed-up work pays off one to one.
  • Split a critical job. A 9 minute end-to-end suite split into three shards of 3 minutes each takes most of it off the path. Splitting has its own costs, which we’ll look at in a later post on test sharding.
  • Remove an edge. Ask of every needs whether the job really needs the other job’s output. Tests that need a built artifact need build; a security scan of the source code probably doesn’t. Each edge you remove can only shorten the run.

The order matters: fix the critical path first, and stop when it moves. After every change, look again, because the next bottleneck is often a job you never thought about.

Ready to optimize your CI/CD pipelines?

Stop guessing where your pipeline time goes. pipescan gives you the insights you need to identify bottlenecks, reduce build times, and ship faster.

Get started for free
© 2026 pipescan. Built for teams who want faster pipelines.