How To

CircleCI Automation: 7 Ways Your Pipeline Burns Credits

Part one of the CircleCI series names the seven patterns that quietly burn credits, from flaky-test reruns and oversized resource classes to cache misses and unattributed spend, each with a detection command. It also shows how to trace a doubled bill one hypothesis at a time, and why a monthly invoice review always finds waste 30 days late.

11 min read
Cover Image for CircleCI Automation: 7 Ways Your Pipeline Burns Credits

CircleCI Automation: 7 Ways Your Pipeline Burns Credits (and How to Catch Them)

The monthly credit report landed and the number had roughly doubled. The team shipped about the same features as last quarter, merged about the same number of PRs and ran about the same test suite. Nobody in the room could point to the workflow, the project or the resource class that ate the difference.

That's the normal failure mode of CircleCI automation. Credits are billed per second, per resource class, across dozens of workflows, and the pipeline that quietly costs the most is almost never the one anyone watches. A flaky test gets rerun three times before it goes green. An oversized resource class runs 40 jobs a day at four times the CPU they need. A workflow rebuilds node_modules on every commit because its cache key never hits. None of it shows up as a broken build. It shows up as a bigger invoice.

The useful part: on teams running roughly 50,000 to 2,000,000 credits a month, the waste concentrates in the same seven patterns nearly every time. This guide covers each one, what it is, why it happens, and how to detect it with a real dashboard path, config.yml pattern or API call. You can run all of these checks in an afternoon.

Four of the patterns bill credits directly:

01Credits

Flaky reruns

Every rerun bills the whole job again, and the flake never gets fixed.

02Credits

Oversized classes

A job bumped to large once and never profiled again.

03Credits

Cache misses

Dependencies rebuilt from scratch because the key never matches.

04Credits

Runaway jobs

Hung steps billed until the no-output timeout finally fires.

Four patterns that bill credits directly. Each one looks like a passing build.

The other three are about visibility. They hide the first four, or make them more expensive to fix:

01Time

Fan-out queueing

Parallel jobs past the concurrency ceiling wait, and teams buy their way out.

02Signal

Masked failures

A green rerun ends the investigation before it starts.

03Ownership

No attribution

One org-level number, so nobody knows which project to fix.

Three patterns that hide the first four, or make them more expensive to fix.

The API calls below use a CircleCI token in $CIRCLE_TOKEN and the project slug format gh/your-org/your-repo (use bb/ for Bitbucket, or the circleci/ slug for GitLab and GitHub App projects).


1. Flaky tests auto-rerunning and burning credits

What it is. A test that passes and fails without any code change. The pipeline goes red, someone clicks Rerun workflow from failed, it goes green, and everyone moves on. Every rerun repeats the job's setup, checkout and test run, and every second is billed again.

Why it happens. Fixing a flaky test is real work. Clicking rerun is free. So reruns become the coping mechanism and the flake never gets fixed. Over months, a handful of chronically flaky tests can double the effective cost of the jobs that contain them.

How to detect it. Insights shows this directly, as long as you upload test results with store_test_results. In the app: Project → Insights → Tests, sorted by most failed and flakiness. Through the API, list the project's flaky tests:

curl -s -H "Circle-Token: $CIRCLE_TOKEN" \
  "https://circleci.com/api/v2/insights/gh/your-org/your-repo/flaky-tests" \
  | jq '.flaky_tests | sort_by(-.times_flaked) | .[:10][] | {test_name, job_name, times_flaked}'

Typical impact. On suites with known flakes, reruns commonly add 10 to 25% to the credit cost of the affected jobs, and far more in developer hours spent waiting.

2. Resource classes oversized for the job

What it is. Each job declares a resource_class (medium, large, xlarge and so on), and the per-minute credit rate climbs with the class. A lint job that needs half a CPU running on large pays the large rate for the whole run, on every commit.

Why it happens. Someone bumped the class once to fix an out-of-memory error or speed up a slow job. It worked, and nobody revisited it. Nobody profiles a job that already passes.

How to detect it. Compare what a job actually uses with the class it's on. Open a recent run and read the Resources tab on the job page, which graphs CPU and RAM for that run. If CPU peaks under 40% and memory sits far below the ceiling, the job is oversized. The class is declared in config.yml:

jobs:
  lint:
    docker:
      - image: cimg/node:20.11
    resource_class: large # profile before trusting this
    steps:
      - checkout
      - run: npm run lint

Typical impact. Dropping one class (say xlarge to large) roughly halves that job's per-minute rate. Across a fleet of oversized jobs, this is often 15 to 30% of compute credits.

3. Workflows without caching, dependencies rebuilt every run

What it is. A job that reinstalls dependencies from scratch on every commit because it has no cache, or a cache key that never matches. The install step runs at full length every time, billing for work the previous run already did.

Why it happens. Caching is opt-in and easy to get subtly wrong. A key with no checksum never invalidates. A key built from a file that changes on every commit never hits. A restore_cache with no matching save_cache means nothing was ever stored.

How to detect it. Read the step timings. Open a few recent runs of the job and compare the install step: if it takes the same long time every run, the cache is missing. The restore_cache step output says it plainly: "Found a cache" or "No cache found". A correct restore and save pair looks like this:

steps:
  - checkout
  - restore_cache:
      keys:
        - deps-v1-{{ checksum "package-lock.json" }}
        - deps-v1-
  - run: npm ci
  - save_cache:
      key: deps-v1-{{ checksum "package-lock.json" }}
      paths:
        - ~/.npm

Typical impact. A cold install commonly adds 30 to 90 seconds per job. On high-frequency pipelines that's easily 10 to 20% of a workflow's credits, recovered almost entirely by one correct cache key.

4. Fan-out that queues on concurrency limits

What it is. A workflow fans out into more parallel jobs (or more parallelism for test splitting) than your plan's concurrency allows. The extra jobs queue. Queue time isn't billed as compute, but it stretches every pipeline, and teams respond by buying more concurrency or bigger classes instead of fixing the fan-out.

Why it happens. Parallelism always makes one run faster in isolation. Nobody accounts for the whole org contending for the same ceiling at 10 a.m. when everyone pushes.

How to detect it. Look for the gap between when a job was queued and when it started. The job details endpoint returns both:

curl -s -H "Circle-Token: $CIRCLE_TOKEN" \
  "https://circleci.com/api/v2/project/gh/your-org/your-repo/job/1234" \
  | jq '{name, queued_at, started_at, resource_class: .executor.resource_class, parallelism}'

At the workflow level, a rising p95 duration with flat median compute is the queueing signature:

curl -s -H "Circle-Token: $CIRCLE_TOKEN" \
  "https://circleci.com/api/v2/insights/gh/your-org/your-repo/workflows?reporting-window=last-30-days" \
  | jq '.items[] | {name, median: .metrics.duration_metrics.median, p95: .metrics.duration_metrics.p95}'

Typical impact. Rarely direct credit waste. But chronic queueing pushes teams into concurrency and resource-class upgrades that add 10 to 20% to spend without making anything faster.

5. Failed workflows nobody re-examines after the rerun goes green

What it is. A workflow fails, gets rerun, passes, and the original failure is never read. Sometimes it was a real regression that a lucky rerun masked. Sometimes it was infrastructure. The signal is thrown away the moment the badge turns green.

Why it happens. Green is the goal, and a green rerun feels like resolution. Reading the failed job means opening it, reading output and thinking. The rerun button needs none of that.

How to detect it. Find the jobs that pass only after retries. Job-level metrics per workflow include success rate and failed runs:

curl -s -H "Circle-Token: $CIRCLE_TOKEN" \
  "https://circleci.com/api/v2/insights/gh/your-org/your-repo/workflows/build-test/jobs?reporting-window=last-30-days" \
  | jq '.items[] | {name, success_rate: .metrics.success_rate, failed_runs: .metrics.failed_runs}'

Typical impact. Hard to price in credits, but every masked regression that reaches production costs far more than the rerun did. A success rate under roughly 90% on a job that "always passes eventually" is the tell.

6. Credit spend nobody attributes per project

What it is. One CircleCI org runs many projects, and the invoice is one number. Nobody knows which project, branch or workflow drives the spend, so nobody can target a fix. The usual outcome is a team tuning a pipeline worth 2% of credits while one worth 40% runs untouched.

Why it happens. CircleCI bills at the org level, and attribution means deliberately pulling per-project data and summing it. Nobody owns that report until the invoice doubles.

How to detect it. In the app, Plan → Plan Usage shows consumption over time and can filter by project. For a ranked list, the org summary endpoint returns credits per project for the window:

curl -s -H "Circle-Token: $CIRCLE_TOKEN" \
  "https://circleci.com/api/v2/insights/gh/your-org/summary?reporting-window=last-30-days" \
  | jq '.org_project_data | sort_by(-.metrics.total_credits_used) | .[] | {project_name, credits: .metrics.total_credits_used}'

For job-level and resource-class detail, the Usage API exports a CSV per org (POST /organizations/{org_id}/usage_export_job).

Typical impact. Attribution rarely saves credits on its own. It's the prerequisite for every other fix in this list. Without it you optimize blind, and the biggest line item stays invisible.

7. Long-running jobs with no timeout

What it is. A job that sometimes hangs (a stuck test process, a network wait, a deadlocked container) and runs until the default no_output_timeout of 10 minutes fires, or until a step that keeps printing reaches the maximum job runtime. Every minute is billed, for a job that was never going to pass.

Why it happens. Timeouts are optional, so most jobs inherit the defaults. A job that hangs once a week goes unnoticed, but each occurrence bills the full runaway duration.

How to detect it. Look for a long tail in job duration: a median of two minutes and a p95 of fifteen means something hangs. The job metrics from pattern 5 include duration_metrics.median and duration_metrics.p95. Cap the runaway with a step-level timeout:

- run:
    name: integration tests
    command: npm run test:integration
    no_output_timeout: 5m

Typical impact. Usually a few percent in aggregate, but it's pure waste, and the fix is one line that fails a hung job in minutes instead of letting it burn.


Tracing a doubled bill, one hypothesis at a time

Back to the report from the opening. "Credits doubled" has four common readings, and each one predicts something different in the data.

  • More runs. Throughput went up (a new scheduled workflow, a bot opening PRs). Predicts: total_runs up roughly in line with credits, credits per run flat.
  • Bigger runs. A resource class went up somewhere. Predicts: runs flat, credits per run up on specific jobs, and a resource_class change in config.yml history.
  • Longer runs. A cache started missing or a test suite grew. Predicts: credits per run up with the same class, and the install or test step longer in the job timings.
  • More reruns. A new flaky test arrived. Predicts: failed_runs up, new entries in the flaky-tests list, and credits concentrated in one or two jobs.

Start with the org summary from pattern 6. It names the project that moved. In that project, the workflow summary from pattern 4 gives runs and credits per workflow, which separates "more" from "bigger or longer". Then the git history settles the class question:

git log -p --since="60 days ago" -- .circleci/config.yml | grep -E "^[+-].*resource_class"

If the class didn't change, the job timings and the flaky-tests list tell you whether it's longer runs or more reruns. Most teams can answer this in an hour once they know which four questions to ask. Without them, it becomes a half-day meeting that ends with "let's watch it next month."


Why the monthly credit report keeps surprising you

"We'll look at credits when the invoice comes" fails for structural reasons, not effort.

The defaults generate waste. Caching is opt-in, timeouts are optional, the resource class is copied from whichever job someone used as a template, and the rerun button is one click. Every new project starts with all four.

Billing is aggregated past the point of action. Credits are metered per second per class per job, and reported as one org-level number per month. The person who could fix a cache key never sees the line item it creates.

The report is a lagging indicator. Every pattern above produces waste on the day it starts, and Insights can see it that day. The invoice shows it 30 days later.

01Every push

Waste starts

A copied config, a new flaky test, a cache key that never hits.

02Same day

Detectable

Insights and job timings already show it, if anyone looks.

03Day 30

Invoice

One org-level number arrives, a month after the meter started.

04Release month

Report skipped

The half-day attribution report loses to shipping, again.

Waste is detectable on the day it starts. The monthly report finds it 30 days later, when it finds it at all.

Here's an illustrative example. A pipeline spending $8,000 a month in credits with 25% addressable waste loses about $2,000 for each month of detection lag. The report that would reveal it takes an engineer half a day, so it gets skipped in a big release month, which is exactly the month that creates the most waste.

Monthly invoice reviewContinuous detection
When waste is found30 days after it startsThe day it starts
GranularityOne org-level numberProject, workflow, job, test
Who sees itWhoever reads the invoiceThe team that owns the job
Flaky testsNoticed when someone complainsRanked by credits burned
Cost to runHalf a day, often skippedRuns on a schedule

The deeper difference is where the work lands. An invoice review asks a person to reconstruct a month of history from an aggregate. Continuous detection reads the same data while it's still small enough to act on, one job at a time.


Where this series goes next

You can make most of this continuous with CircleCI's own tools. Part two builds it hands-on: Insights and its API, test splitting, cache keys that hit, approval jobs, rerun with SSH and v2 API reporting with curl.

Part three covers what's left after that: classifying each failure, deciding fix versus quarantine, and attributing credits without a half-day report. CloudThinker connects to CircleCI with a read-only token, watches workflows continuously and proposes fixes. It starts in Manual, so every change waits for a person's approval, and approval jobs always stay human. A good first question:

"Which project's credit spend grew the most over the last 30 days, and is it more runs, bigger resource classes, longer jobs or more reruns?"

Try CloudThinker free with 100 premium credits and no card required, or continue with part two.

circlecicicddevopsautomationflakybuildscreditwaste