Guides / Too many small jobs

Too much work per run

Too many small jobs, too much overhead

By Keith Mazanec, Founder, CostOps ยท Updated January 30, 2026

A team splits their CI into 8 separate jobs: lint, typecheck, format, unit tests, integration tests, build, security audit, license check. Each job checks out the repo and installs dependencies from scratch. The actual checks take seconds. The setup takes a minute. And GitHub bills every job rounded up to the nearest minute, which means 8 jobs that each run for 40 seconds cost 8 full minutes. Consolidating those into 2–3 jobs cuts the bill in half without losing any signal.

Symptoms

How to tell if job overhead is inflating your bill

Open your repository's Actions tab and click into a recent workflow run. Look at the job list and their durations:

  • Setup dominates runtime. Expand any job and compare the time spent on actions/checkout, actions/setup-node, and npm ci versus the actual check. If setup is 45 seconds and the lint step is 8 seconds, 85% of that job's cost is overhead.

  • Many jobs under 1 minute. GitHub rounds each job up to the next whole minute. A job that finishes in 12 seconds is billed as 1 minute. Ten such jobs cost 10 billed minutes for under 2 minutes of actual compute. This is the single biggest source of billing waste from job sprawl.

  • Identical setup steps across jobs. Every job in the workflow runs the same checkout → setup-node → npm ci sequence. If you have 8 jobs, you're running npm ci 8 times per workflow invocation, downloading and extracting the same packages each time. A working dependency cache can reduce this cost but doesn't eliminate it.

  • High job count relative to workflow duration. A workflow that takes 3 minutes wall-clock but has 10 jobs is billing 10+ minutes. The parallelism feels fast for developers but costs 3–4× more than a sequential approach would.

Metrics

The per-minute rounding tax

GitHub bills each job's runtime rounded up to the nearest minute. This rounding is per-job, not per-workflow. The more jobs you have, the more rounding waste you accumulate. Here's a typical scenario for a Node.js project on Linux runners:

Before: 8 granular jobs

Jobs per run 8
Avg actual time/job 0:48
Billed minutes/run 8
Runs/day 30
Monthly cost $32/mo

8 jobs × 1 min × 30 runs × 22 days × $0.006/min

After: 3 consolidated jobs

Jobs per run 3
Avg actual time/job 1:50
Billed minutes/run 6
Runs/day 30
Monthly cost $24/mo

Save $8/mo · $100/year · per workflow

That's a single workflow on Linux at $0.006/min. On macOS runners at $0.062/min, the same 8-job workflow costs $328/mo and drops to $246/mo after consolidation, saving $82/mo. The rounding penalty hits harder the more jobs you have and the more expensive the runner.


Fix 1

Group fast checks into a single job

Lint, typecheck, format check, and security audit are all fast, read-only operations that need the same setup: checkout the code and install dependencies. There is no technical reason for these to be separate jobs. Running them as sequential steps in one job means one checkout, one install, one billed unit.

The tradeoff is that a failure in an earlier step prevents later steps from running. If the lint check fails, you won't see typecheck results until lint is fixed. In practice, this is rarely a problem because developers fix the first error and push again. If you want all checks to report independently, use continue-on-error: true on individual steps and check the outcomes at the end.

4 jobs → 4 billed minutes
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm run lint
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx tsc --noEmit
  format:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm run format:check
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm audit --audit-level=high
1 job → 2 billed minutes
jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npx tsc --noEmit
      - run: npm run format:check
      - run: npm audit --audit-level=high

Four jobs at under a minute each bill as 4 minutes. One job running all four checks sequentially finishes in about 1:30 and bills as 2 minutes. Same checks, half the cost, and only one dependency install instead of four.

Fix 2

Build once, share with downstream jobs

When you do need multiple jobs, such as running unit tests and integration tests in parallel, the worst pattern is having each job independently install dependencies and build the project. A better approach is a single build job that prepares the workspace, then passes the result to downstream jobs via artifacts. Our build once, use everywhere guide covers this pattern in depth.

The actions/upload-artifact and actions/download-artifact actions transfer files between jobs. Downloading an artifact is significantly faster than running npm ci from scratch, typically taking 5–15 seconds versus 30–120 seconds.

.github/workflows/ci.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          cache: npm
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: |
            node_modules
            dist

  unit-tests:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: build-output
      - run: npm test

  integration-tests:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: build-output
      - run: npm run test:integration

The downstream jobs skip npm ci and npm run build entirely. They download the pre-built artifact in seconds and go straight to running tests. This eliminates the most expensive repeated step.

One caveat: artifact upload and download are not free in terms of time. Large node_modules directories (hundreds of MB) can take 20–30 seconds to upload and download. For small projects where npm ci takes 15 seconds, artifact sharing may not save anything. Measure both approaches, since this fix pays off most when the install or build step is expensive.

Fix 3

Use continue-on-error for independent checks in one job

The main objection to consolidating jobs is losing independent failure reporting. If lint fails, you want to still see whether typecheck and format checks pass. With separate jobs, each reports its own status. With a single job, the first failure stops everything by default.

The fix is continue-on-error: true on each check step, combined with a final step that evaluates all outcomes. Every check runs regardless of earlier failures, and the job still fails if any check failed.

.github/workflows/ci.yml
jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          cache: npm
      - run: npm ci

      - name: Lint
        id: lint
        continue-on-error: true
        run: npm run lint

      - name: Typecheck
        id: typecheck
        continue-on-error: true
        run: npx tsc --noEmit

      - name: Format
        id: format
        continue-on-error: true
        run: npm run format:check

      - name: Check results
        if: always()
        run: |
          echo "Lint: ${{ steps.lint.outcome }}"
          echo "Typecheck: ${{ steps.typecheck.outcome }}"
          echo "Format: ${{ steps.format.outcome }}"
          if [[ "${{ steps.lint.outcome }}" == "failure" ||
                "${{ steps.typecheck.outcome }}" == "failure" ||
                "${{ steps.format.outcome }}" == "failure" ]]; then
            exit 1
          fi

This gives you the best of both worlds: a single billed job with one setup phase, but full visibility into which checks passed and which failed. The job summary in the Actions UI shows each step's status individually.


Reference

When separate jobs are worth the overhead

Consolidation is not always the right call. Keep jobs separate when the overhead pays for itself:

Keep separate when… Why
Jobs need different runners A Linux lint job and a macOS build job can't share a runner. Separate jobs are the only option.
Each job runs 5+ minutes The per-minute rounding penalty is small relative to runtime. Parallelism saves wall-clock time with minimal billing waste.
Jobs need different services Unit tests don't need PostgreSQL. Integration tests do. Running them separately avoids paying for a database container during the unit test phase.
Job is a required status check Branch protection rules reference job names. If "lint" is a required check, it needs to be a separate job (or you need to update your branch protection config).

The rule of thumb: consolidate checks that are fast, share the same setup, and run on the same runner type. Keep jobs separate when they're long-running, need different environments, or serve as distinct required status checks.

Reference

Overhead per GitHub Actions job

Every job on a GitHub-hosted runner pays a fixed cost before your code runs. Here's what that overhead looks like for a typical Node.js project:

Phase Typical Notes
Runner provisioning 5–20s Queue + VM startup
actions/checkout 2–5s Depends on repo size
actions/setup-node 1–3s Cached on runner
npm ci (no cache) 30–120s Downloads from registry
npm ci (with cache) 10–30s Restores ~/.npm cache
Total overhead/job 18–158s Before your code runs

If your actual check takes 8 seconds and overhead is 45 seconds, you're paying for 53 seconds of work, all billed as 1 minute. Multiply that by 8 jobs and you're billing 8 minutes for roughly 1 minute of useful compute. That's the job sprawl tax.

Related guides

Guides / Too many small jobs

See which jobs are costing more than they should

CostOps breaks down billable minutes per job, shows setup-to-work ratios, and flags workflows where consolidation would save the most.

Free for 1 repo. No credit card. No code access.

Built by engineers who've managed CI spend at scale.