Skip to content

Signals

What makes your CI slow, costly or busier than it needs to be, and which workflows it affects.

The Signals page, under Analytics, points out patterns in your CI that cost you time or money. Each signal has the same title every time, with the repos and workflows it affects listed underneath, most minutes first.

Signals are grouped by the question they answer:

  • Speed: why is CI slow?
  • Cost: why does CI cost this much?
  • Complexity: is CI doing more than it needs to?

Below the signals, the Breakdown shows who and what uses your CI minutes.

Every area shows on the page, even when none of its signals came up. Signals that didn’t come up are listed under their area, after No sign of, so you can see everything the page checks for. The ⓘ beside any signal explains it and links to its section here.

Click a row under any signal to see the runs behind it, newest first. Each run opens in History with its jobs, and on signals about one job, that job.

The page updates every hour. A signal shows up once it covers at least 30 minutes and 1% of all your job minutes in the range, so small one-offs stay off the page. Minutes and spend sit in separate columns. Spend uses the same list prices as the Usage page, so public repos and self-hosted runners count as free.

Waiting for a runner and waiting on one job cost time rather than money, so those two signals leave the spend column empty.

These jobs waited more than a minute after GitHub queued them before a runner took them, grouped by their runs-on labels. Jobs wait when every runner with those labels is busy, or when the account reaches GitHub’s limit on jobs running at once. Add a step to your runner order so jobs go to the next runner that’s free instead of waiting.

Click a runner’s name to open its panel, with its jobs per day and recent jobs. When every waiting job ran on the same machine, the name opens that machine.

This job ran alongside the others in its run and was still going after they all finished, so the run waited on it alone. The minutes are that wait. Jobs that wait for another job with needs aren’t counted, since they start after it by design. Split the job, or compare its times on each runner on the Performance page.

These jobs failed in one of their steps and then passed when the run was tried again, so the failure likely had nothing to do with the change. The minutes and spend are the failed attempts. Fixing the test or step that fails now and then saves the re-run and the wait for it.

These jobs failed before any of their steps started, grouped by their runs-on labels. The runner was the problem, not your code. Turn on automatic retries to send them to another runner.

These runs kept going after someone pushed again to the same branch. The minutes count from when the newer run started. Your default branch is left out, since most teams want every commit there to finish.

Add a concurrency group with cancel-in-progress to the workflow, and GitHub cancels the older run when a newer one starts:

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

The workflow ran for the same commit from both push and pull_request. The push runs are the extra ones, so their minutes and spend are what the page shows. Limit push to your default branch and the pull request run still covers every change.

GitHub bills each job by the whole minute. These jobs finished in under a minute on a billed runner, so part of every minute went unused. The page shows that unused part. Lint and other quick checks can run as optimized checks instead.

The workflow ran for a pull request that only changed docs: Markdown and other prose files, anything in a docs folder, or a license. For runs without a pull request, it goes by the title instead: commits titled as docs changes, such as docs: fix a typo in Conventional Commits. Workflows with “doc” in their name are left out, since building docs is their job. Add paths-ignore to the workflow’s push and pull_request triggers so changes that only touch docs skip it:

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

The workflow started 10 or more jobs for a push or pull request on a branch other than your default, usually a wide matrix. All of those runs’ minutes count. Running a smaller matrix before merge, and the full one on your default branch, still catches most problems early.

The Breakdown splits all your job minutes and spend one way at a time:

  • Person: who started the run.
  • Repository.
  • Workflow, with its repo.
  • Branch.
  • Trigger: push, pull request, schedule, manual or re-run.
  • Changed folder: the folders a pull request changed, with their repo, such as src or docs. Inside folders like apps and packages, each project is its own folder, such as apps/web. Only pull request runs count here, and a run counts under every folder it changed, so the rows overlap.

Each view lists the 10 with the most minutes, then everything else in one row. Runs count once each, so a run with a 20-job matrix is still one run.

To know which folders a pull request changed, the dashboard reads the names of its files from GitHub once an hour. It keeps only the folders, never the files’ contents.

Click a row in the Breakdown to narrow the whole page to it: the signals, their rows and the other Breakdown views then only count that person’s, repo’s, workflow’s, branch’s, trigger’s or folder’s runs. Click a row in another view to narrow it further, and use the × beside a filter to remove it.

A filtered page also shows that filter’s minutes per day, with the same number of days before as a dashed line, so you can see whether it’s growing. Filtered pages cover up to the last 30 days.