Nov 17, live webinar: ECS to EKS without freezing the roadmap for a quarter

20 ways to speed up GitHub Actions and cut its cost, ranked by impact

Twenty changes that make GitHub Actions faster and cheaper, ranked from the biggest saving to the smallest, with the YAML to copy, 2026 prices, real numbers from Sentry, Cal.com, Mercari and PostHog, and what changes when coding agents open most of your pull requests.

Romaric Philogene
CEO & Co-founder
NOV 15, 2026 · 15 MIN
20 ways to speed up GitHub Actions and cut its cost, ranked by impact

Key Points:

  • The cheapest minute is the one you don't run. Cancelling superseded runs and skipping code that didn't change are the two biggest savings, and both take minutes to set up.
  • Caching comes next. Docker layer caching and dependency caching remove most of the repeated work in a typical pipeline.
  • Runner choice is mostly about cost. Arm runners cost 17% to 40% less per minute than x64 runners of the same size, and a 1-vCPU runner costs a third of the standard one.
  • Coding agents multiply the number of runs. An agent that pushes ten small commits to a pull request starts ten pipelines. Concurrency groups, draft pull requests and timeouts keep that under control.
  • GitHub Actions is cheap per minute. A standard Linux runner costs $0.006 a minute in 2026. Moving the build elsewhere makes sense for speed or for one pipeline from commit to running code, rarely for the price alone.

Qovery · Infrastructure control plane
Build with Claude Code, Deploy with Qovery
Learn more

Why this matters more in 2026

GitHub Actions workflows were written for people. A developer pushes a few times a day, waits for the checks, and merges.

Coding agents changed the rhythm. Claude Code, Codex, Cursor and Copilot open pull requests on their own, push fixes in small commits, and push again when a check fails. Each push starts every workflow from the top. A team that ran 50 workflows a day can run 500, and two things grow with it: the bill, and the time the agent, and the person reviewing its work, waits for a green check.

The 20 changes below are ranked by how much they usually save, starting with the biggest. Each one says what it saves, how long it takes to set up, and where the numbers come from. Numbers published by companies that sell CI products are marked as vendor figures.

Before you start: find where the time goes

Don't guess. GitHub shows average run time, queue time and failure rate per workflow, job and runner type under your organization's Insights > Actions Performance Metrics, over up to 100 days, with a CSV export.

For one repository, the GitHub CLI gives you the timing of every step:

BASH|Recent runs of one workflow
gh run list --workflow ci.yml --limit 50 \
  --json databaseId,createdAt,updatedAt,conclusion

# Time per step for one run
gh run view <run-id> --json jobs \
  --jq '.jobs[].steps[] | [.name, .startedAt, .completedAt]'

You are looking for one or two jobs that take most of the minutes, and for the time jobs spend waiting in the queue. Write down the median and the slowest runs before you change anything, so you can tell whether a change helped.

Two billing details matter for everything below. GitHub rounds every job up to the next whole minute, so eight jobs of 40 seconds bill eight minutes. And each plan has a limit on jobs running at once: 20 on Free, 40 on Pro, 60 on Team and 500 on Enterprise for standard runners. Jobs above the limit wait in the queue.

1. Cancel runs nobody will read

Saves: a large share of minutes on busy branches, and queue time. One team reported about 40% fewer minutes a month after adding it (source). Effort: 5 minutes.

When someone, or an agent, pushes twice to the same pull request, the first run is already useless. By default it keeps running, and you pay for it. A concurrency group cancels it:

YAML
concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

The condition limits cancelling to pull requests, so deployments and main-branch runs always finish. Cancelled runs also free their slots for the next jobs in the queue.

2. Skip code that didn't change

Saves: the most of any change in a monorepo. Sentry cut its fastest CI run from 20 to 13 minutes with Nx caching (source). Mercari cut pull request job time by 30% with a Turborepo remote cache (source). Effort: 15 minutes for path filters, hours to days for a monorepo build tool.

For a single app, path filters skip whole workflows when only docs or config changed:

YAML
on:
  pull_request:
    paths-ignore:
      - "**.md"
      - "docs/**"

In a monorepo, test only the packages a change touches, and share task results between runs with a remote cache:

BASH
npx nx affected -t test --base=origin/main
turbo run test --filter=...[origin/main]

One trap: if a workflow is a required status check and paths skips it, the pull request waits for that check forever. Put the filter inside the job instead (for example with dorny/paths-filter) and keep one small job that always runs and reports the result.

3. Cache Docker layers and order your Dockerfile

Saves: most of the image build when dependencies didn't change, because unchanged layers are skipped. PostHog's average image build went from 16 to 3 minutes with a persistent cache on Depot's builders (vendor case study, source). Effort: 30 minutes.

Give BuildKit a cache that survives between runs:

YAML
- uses: docker/setup-buildx-action@v3

- uses: docker/build-push-action@v7
  with:
    context: .
    push: true
    tags: ghcr.io/acme/api:${{ github.sha }}
    cache-from: type=gha,scope=api
    cache-to: type=gha,mode=max,scope=api

Give each image its own scope, or they overwrite each other's cache. The gha cache shares your repository's Actions cache budget. For large images, a registry cache (type=registry,ref=ghcr.io/acme/api:buildcache) has no size limit and is usually a bit faster.

Then order the Dockerfile so a code change doesn't throw away the dependency layer. Copy the lockfile, install, and only then copy the source:

DOCKERFILE
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

Add a .dockerignore that excludes node_modules, .git and build output, so the build context stays small. One limit to know: RUN --mount=type=cache mounts don't survive between runs on GitHub-hosted runners. You need a persistent builder for that. We wrote more on Dockerfile order in 3 tips to improve your Dockerfile build time.

4. Cache dependencies

Saves: usually 1 to 3 minutes per job on a cold install (practitioner reports). Effort: 10 minutes.

The official setup actions cache the package manager's store for you:

YAML
- uses: actions/setup-node@v5
  with:
    node-version: 22
    cache: pnpm

actions/setup-python, actions/setup-go and actions/setup-java have the same option. For anything else, use actions/cache with a key built from the lockfile:

YAML
- uses: actions/cache@v6
  with:
    path: ~/.cache/my-tool
    key: ${{ runner.os }}-deps-${{ hashFiles('**/lockfile') }}

GitHub keeps 10 GB of cache per repository by default and evicts the least recently used entries. Since November 2025, paid plans can raise that limit and keep entries longer. Pull requests can read the cache of their base branch, so make sure the main branch saves one. Measure the restore time: a very large cache can take longer to download than a fresh install.

5. Run jobs in parallel and shard slow tests

Saves: wall-clock time, not minutes. Cal.com cut its pull request wait from 30 to 5 minutes by sharding tests, splitting jobs, caching and moving to faster runners (source). Effort: 1 to 4 hours.

Jobs without needs: run at the same time. Split lint, type checks, unit tests and the image build into separate jobs, and the pipeline takes as long as the slowest one instead of their sum. Then split the slowest test suite:

YAML
jobs:
  e2e:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v5
      - run: npx playwright test --shard=${{ matrix.shard }}/4

Every shard repeats the checkout and install, and each job is rounded up to a full minute, so sharding usually costs slightly more minutes. Don't add more shards than your plan's concurrency limit, or they wait in the queue.

6. Move to faster runners with a persistent cache

Saves: time on every job, with figures that are mostly vendor claims. Blacksmith and Depot advertise about 2x faster runs. Cal.com measured cache downloads twice as fast on Blacksmith, about 22 seconds saved per job (source). PostHog measured a 12% to 13% shorter average job on Depot's runners (source). Effort: about an hour.

Blacksmith, Depot and Namespace run your existing workflows on their own machines. You usually change one line, the runs-on: label. Much of their speed comes from caches that stay on fast local disks between runs, instead of downloading from GitHub's cache service each time. At their November 2026 list prices, Blacksmith charges $0.004 a minute for 2 vCPU, Namespace about $0.002 to $0.003, and Depot $0.04 a minute for a Docker build on its builders, on top of a monthly plan.

7. Use Arm runners

Saves: 17% to 40% of the cost per minute. Effort: 10 minutes, plus checking that your images and tools support arm64.

Since January 2026, Arm runners are available in private repositories. GitHub's prices per minute:

SizeLinux x64Linux arm64
2-core$0.006$0.005
4-core$0.012$0.008
8-core$0.022$0.014
YAML
runs-on: ubuntu-24.04-arm

Most Node.js, Python and Go workloads run on Arm without changes. Check that your Docker base images publish an arm64 variant.

8. Run small jobs on the 1-vCPU runner

Saves: two thirds of the cost of jobs that do almost nothing. Effort: 5 minutes.

Labelers, notifications and small scripts don't need two cores. The ubuntu-slim runner (1 vCPU, 5 GB of RAM) costs $0.002 a minute, a third of the standard runner. Jobs on it can run for at most 15 minutes.

YAML
runs-on: ubuntu-slim
Build and deploy from one place, on your own cloud.
Qovery builds your images in your cloud account, deploys them, and gives every pull request its own environment. Builds are included in your plan.

9. Use larger runners only for jobs that use the cores

Saves: money or time, depending on the job. Effort: an hour of testing.

An 8-core x64 runner costs $0.022 a minute, 3.7 times a 2-core runner. It only saves money if the job runs at least 3.7 times faster. Parallel compiles and large test suites sometimes get there. Jobs that mostly wait on the network or the disk don't. Try it on your slowest job and compare the minutes billed. Larger runners need a Team or Enterprise plan.

10. Run only the tests a change can affect

Saves: a large part of a big test suite. Vendors of test selection tools claim 60% to 80% less test time (vendor figures, source). Effort: a few hours.

Free tools already do a simple version:

BASH
npx jest --changedSince=origin/main
pytest --testmon

Run the full suite on the main branch or nightly anyway. Test selection misses changes to config, fixtures and anything outside the code it tracks.

11. Run the heaviest suite once, in the merge queue

Saves: one full run of your slowest suite for every push to a pull request. Effort: an hour.

Run cheap checks on every push, and the slow end-to-end or integration suite only when the pull request enters GitHub's merge queue:

YAML
on:
  pull_request:
  merge_group:

jobs:
  e2e:
    if: github.event_name == 'merge_group'

GitHub's merge queue still builds every entry. Third-party queues such as Mergify, Graphite and Trunk can test several pull requests in one batch. Make sure your required checks also report on merge_group, or the queue stalls.

12. Keep coding agents from burning minutes

Saves: a fast-growing share of minutes on repositories where agents open pull requests. Effort: 30 minutes.

  • Cancel superseded runs on agent branches. Agents push often and fast. Tip 1 matters most here.
  • Give the agent its tools up front. For Copilot coding agent, install dependencies in .github/workflows/copilot-setup-steps.yml so the agent doesn't spend minutes trying installs by trial and error.
  • Limit what triggers an agent. For anthropics/claude-code-action, run it on explicit mentions or labels, set timeout-minutes, and cap the number of turns with --max-turns.
  • Keep the approval step. By default, workflows on Copilot's pull requests wait for a person to click "Approve and run workflows". Since March 2026 admins can skip it. That's faster, but the workflows then run with your secrets on code nobody has read.
  • Give each pull request a running environment. Tests tell you the code works. A reviewer still needs to use what the agent built. A preview environment per pull request, deleted on merge, does that. We walked through one setup in Claude Code + GitHub Actions: deploy every Claude PR to its own environment.

13. Skip heavy suites on draft pull requests

Saves: the expensive suites on every push while a pull request is still in progress. Effort: 15 minutes.

Let people and agents iterate in draft pull requests that run only the cheap checks. The full suite runs once, when the pull request is ready:

YAML
on:
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review]

jobs:
  e2e:
    if: github.event.pull_request.draft == false

You can also run a suite only when someone adds a label:

YAML
    if: contains(github.event.pull_request.labels.*.name, 'run-e2e')

Skipped jobs count as passed for required checks. Make sure that's what you want.

14. Set a timeout on every job

Saves: nothing on a healthy run, and a lot on a broken one. Effort: 5 minutes.

A job that hangs runs for 6 hours before GitHub stops it. That's $2.16 on a 2-core runner and $58 on a 64-core one, for a single job. Set a timeout at two to three times the job's usual duration:

YAML
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15

This matters even more for jobs that run coding agents, which can loop.

15. Run cheap checks first

Saves: the expensive jobs on every run that fails early. Effort: 30 minutes.

If the type check fails, there's no point running 20 minutes of end-to-end tests. Make expensive jobs wait for cheap ones:

YAML
jobs:
  e2e:
    needs: [lint, unit]

This adds the lint time to every passing run, so gate only the expensive jobs and keep the gate under a minute or two.

16. Build the image once and deploy that exact image

Saves: one full image build for every extra environment. Effort: 1 to 2 hours.

Many pipelines build the same image for tests, staging and production. Build it once, push it, and pass along its digest (@sha256:...), which docker/build-push-action returns as an output. Tests run against that image, and every environment deploys that same digest. You save the rebuilds, and production runs the image your tests checked. Keep environment settings out of the image and pass them at runtime.

17. Pass build output between jobs instead of rebuilding

Saves: a rebuild in each job that needs the output. GitHub says version 4 of the artifact actions uploads and downloads up to 98% faster than version 3 (source). Effort: 15 minutes.

YAML
- uses: actions/upload-artifact@v4
  with:
    name: dist
    path: dist/
    retention-days: 1

Use a short retention: artifacts count against your plan's storage.

18. Keep the checkout small

Saves: little in most repositories, a lot in large monorepos and repositories with Git LFS. Effort: 5 minutes.

actions/checkout already fetches only the last commit. Don't set fetch-depth: 0 unless a tool needs the history, and if it does, filter: blob:none fetches history without file contents. In a monorepo, sparse-checkout fetches only the folders a job needs. If you use Git LFS, cache the LFS objects instead of downloading them on every run.

19. Stop installing the same tools on every run

Saves: usually tens of seconds per job. Effort: 30 minutes to 2 hours.

GitHub's hosted images already include most toolchains, so check before installing. Avoid a blanket apt-get update in every job. Install only what's missing, cache the packages, or use a small prebuilt image with your tools in GitHub's container registry. A large image takes longer to pull than the install it replaces, so keep it small.

20. Clean up scheduled workflows

Saves: minutes nobody looks at. A 5-minute job that runs every hour uses about 3,600 minutes a month. Effort: 15 minutes.

Check the Actions usage metrics for scheduled workflows and remove or slow down the ones nobody reads. Public repositories disable schedules after 60 days without activity. Private repositories don't.

When to move the build out of GitHub Actions

After these changes, GitHub Actions is a fast and cheap place to run most CI. You have two reasons to look further: you want more speed per dollar (tip 6), or you want the build to be part of the path to a running environment instead of a separate pipeline you maintain.

The second one is what Qovery does. When you push, Qovery builds the image in your own cloud account, stores it in your registry, deploys it to your Kubernetes cluster, and can create a preview environment for the pull request.

We spent October and November making those builds faster, because our own customers compared them with GitHub Actions, and we were slower. On our public benchmark, the wait from push to image ready is now faster than GitHub Actions on Python, server-rendered front ends and large images, and on par for Node.js, React and PHP. An empty build is still slower. Across all builds on Qovery, the median build step went from 243 seconds in May to 84 seconds in November. We wrote down what we measured and what we fixed in Making Qovery builds as fast as GitHub Actions, and then faster.

Build minutes on Qovery are included in your plan's deployment minutes. Inside that allowance, a build costs nothing extra. Above it, GitHub Actions and the runners in tip 6 are cheaper per minute. Qovery makes sense when you want one pipeline from commit to running code, builds that never leave your cloud account, and an environment for every pull request. If all you want is the lowest price per minute, use the tips above.

Many teams combine both: tests run on GitHub Actions or a faster runner, and Qovery builds the image once and deploys it everywhere.

Summary

RankChangeImpactEffort
1Cancel superseded runsHigh5 min
2Skip code that didn't changeHigh15 min to days
3Docker layer cache and Dockerfile orderHigh30 min
4Dependency cacheHigh10 min
5Parallel jobs and test shardsHigh (time)1 to 4 h
6Faster runners with a persistent cacheHigh (vendor figures)1 h
7Arm runnersMedium (cost)10 min
81-vCPU runner for small jobsMedium (cost)5 min
9Larger runners only for CPU-bound jobsMedium1 h
10Test selectionMedium to highHours
11Heavy suite only in the merge queueMedium1 h
12Limits on coding agentsMedium, growing30 min
13Skip heavy suites on draftsMedium15 min
14Timeouts on every jobMedium (caps risk)5 min
15Cheap checks firstMedium30 min
16Build once, deploy the same imageMedium1 to 2 h
17Artifacts instead of rebuildsMedium15 min
18Small checkoutLow to medium5 min
19No repeated tool installsLow to medium30 min to 2 h
20Scheduled workflow cleanupLow to medium15 min
Frequently asked questions
Why is my GitHub Actions workflow slow?

Usually one of four reasons: jobs wait in the queue for a runner, dependencies install from scratch on every run, the Docker build has no layer cache, or one long test job holds up the rest. Actions Performance Metrics in your organization's Insights, or the step timings from the GitHub CLI, show which one.

How do I cache Docker layers in GitHub Actions?

Use the docker/setup-buildx-action and docker/build-push-action actions with the gha cache backend in mode max, and give each image its own scope. For large images, use a registry cache instead. Order your Dockerfile so dependencies install before the source code is copied.

How much does GitHub Actions cost per minute in 2026?

A standard 2-core Linux x64 runner costs $0.006 a minute and the 2-core Arm runner $0.005. The 1-vCPU Linux runner costs $0.002. Larger runners cost roughly twice as much with each doubling of cores, and every job is rounded up to the next whole minute. Windows and macOS runners cost more.

Do larger runners save money?

Only when the job finishes in proportionally less time. An 8-core runner costs 3.7 times a 2-core one, so the job must run at least 3.7 times faster to cost the same. Parallel compiles sometimes get there. Network-bound jobs rarely do.

How do I stop coding agents from burning GitHub Actions minutes?

Cancel superseded runs with a concurrency group, let agents work in draft pull requests that skip expensive suites, set a timeout on every job, trigger agent workflows only on explicit mentions or labels, and keep the approval step before workflows run on agent pull requests.

Is the GitHub Actions cache limited to 10 GB?

10 GB per repository is the default. Since November 2025, paid plans can pay for more cache storage and keep entries longer. When the limit is reached, GitHub evicts the least recently used entries.

Should I build Docker images in GitHub Actions or in my deployment platform?

Building in GitHub Actions is cheap and works well when you deploy with your own scripts. Building in the deployment platform, as Qovery does, removes a separate pipeline, keeps images in your own cloud account, and lets the same build feed preview, staging and production environments.

Romaric Philogene
About the author
Romaric Philogene

Romaric founded Qovery to make Kubernetes accessible to every engineering team. He writes about platform strategy, developer experience, and the future of cloud infrastructure.

Next step

Build and deploy from one place, on your own cloud.

Qovery builds your images in your cloud account, deploys them, and gives every pull request its own environment. Builds are included in your plan.