Skip to content
Build and shipProduct pagePricing

GitHub runners

GitHub runners give each GitHub Actions job a fresh microVM, on your own servers or on the Frazer fleet, next to Frazer Cache. A pool splits its capacity into lanes, so pull requests opened by agents get their own floor, cap and daily budget and never starve the jobs people are waiting for. To move a workflow, you change one line: runs-on.

Product overview: GitHub runners.

You install the Frazer GitHub Runners app on your GitHub organization and create a pool. When a job asks for a Frazer label, Frazer starts a microVM, registers a just-in-time runner for that one job, attaches the repository’s warm disk and points the job’s tools at the cache. When the job ends, the VM is destroyed. A runner never takes a second job, so nothing survives from one job to the next: no files, no Docker state, no credentials.

Scaling follows the queue. For each lane, Frazer adds runners for the jobs GitHub has assigned, keeps a warm pool learned from the last 14 days of arrivals, and stays inside the lane’s floor, cap and budget, the pool’s minimum and maximum, and the free capacity of its servers. On your own servers, CPU is overcommitted, because CI jobs rarely use every core they ask for. An optional overflow pool on the Frazer fleet takes jobs when your servers are full.

A pool belongs to a host project, usually the project that owns your servers (for example acme-infra), and is shared with the projects whose repositories run on it. Each repository belongs to one project per pool. That project pays for the repository’s jobs and sees them under Engineering › Runners.

  • Pool: runner capacity for one GitHub connection: placement, sizes, lanes, isolation, image, cache policy, egress policy and scaling. A pool is mirrored to a GitHub runner group with selected repositories.
  • Size: a runs-on label with its vCPU, memory and disk, such as frazer-8c.
  • Lane: a priority class inside a pool, chosen by a suffix on the label (frazer-8c-agent).
  • Runner: one job’s VM. Read-only: you see its state, server, job and lane.
  • Runner image: what a job’s VM boots, versioned and rolled out in rings.
  • Job: the record of one job: phases, tests, cache restores, result and runner-seconds.
Label vCPU Memory Disk
frazer-4c 4 16 GiB 75 GiB
frazer-8c 8 32 GiB 100 GiB
frazer-16c 16 64 GiB 150 GiB
Level What runs the job Use it for Where
microvm (default) A microVM with its own kernel and its own Docker daemon Every job, including pull requests and agents; services:, testcontainers, docker build Your servers and the fleet
vm A full VM per job from a warm pool Whole GPUs, nested virtualization (Android emulators, VM tests), very large disks Your servers and the fleet; GPU runners run on your GPU servers
container A plain container, with a rootless builder for image builds Trusted repositories that need the fastest start Your servers only, in pools without forks or agent lanes

You need the right to install a GitHub App on your GitHub organization, and a server pool in the host project (or a Frazer fleet location) for the runners to run on.

  1. Install the Frazer GitHub Runners app. In the console, open your organization’s Settings › Integrations, choose GitHub and install Frazer GitHub Runners on your GitHub organization. The installation becomes a connection, such as github-acme.

    The App asks for organization Self-hosted runners: write, repository Actions: write and Metadata: read, and the workflow_job and workflow_run events. It has no access to your code and no administration permission.

  2. Create the pool in the host project.

    Terminal window
    frazer githubrunners pools create acme-ci --provider github --connection github-acme \
    --placement pool:vint-hill-default --sizes frazer-4c,frazer-8c,frazer-16c \
    --isolation microvm --cache-policy acme-default --dry-run

    Read the plan, then run the same command without --dry-run to apply it. If a repository’s workflow uses pull_request_target, the plan names the workflow file: trusted pools refuse it.

  3. Share the pool with the projects that run on it, and name each project’s repositories.

    Terminal window
    frazer githubrunners pools share acme-ci --project acme-infra \
    --consumer acme-staging=repositories:notes,platform-envs --dry-run
  4. Change runs-on in a workflow to a Frazer size.

    .github/workflows/ci.yml
    jobs:
    test:
    runs-on: ubuntu-latest
    runs-on: frazer-8c
    steps:
    - uses: actions/checkout@v5
    - run: go test ./...

    actions/cache keeps working with no edits: the job’s cache calls go to Frazer Cache.

  5. Push a commit and open the job in the consumer project under Engineering › Runners › Jobs, or list jobs from the CLI.

    Terminal window
    frazer githubrunners jobs list --project acme-staging --since 24h

    Each job shows five phases: Wait, Start, Cache, Steps and Upload.

GitHub hands a queued job to any idle runner whose labels match, so priority between kinds of jobs needs separate labels. Each lane is its own set of runners with its own label suffix, and Frazer divides the pool’s capacity between lanes in this order:

Lane Label suffix Runs Default
Release -release Release workflows, on frazer-16c-release Floor of 8 runners
Main -main Pushes and the merge queue (merge_group) Floor of 6 runners
Human PR none (frazer-8c) Pull requests opened by people —
Agent PR -agent Pull requests opened by agents Floor of 4, cap of 40 % of the pool’s maximum, daily budget
Batch -batch Scheduled workflows —

Print the line for a pool, and paste it into your workflows:

Terminal window
frazer githubrunners pools snippet acme-ci
.github/workflows/ci.yml
runs-on: frazer-8c${{ github.event_name == 'pull_request' && (github.event.pull_request.user.type == 'Bot' && '-agent' || '') || '-main' }}

Pull requests opened by bot accounts go to the agent lane, other pull requests to the human lane, and everything else to the main lane. A workflow that keeps a plain frazer-8c runs in the human lane; release workflows use frazer-16c-release and scheduled workflows the -batch suffix. A pool can also treat agent branches, or a list of agent accounts in your organization, as agents; the snippet then includes them.

The agent lane has four controls, set per pool:

  • Cap: at most 40 % of the pool’s maximum runners by default.
  • Daily budget: 1,900 runner-minutes a day per pool by default. When the budget is used up, the lane is held: its jobs stay queued in GitHub, marked Held: agent budget, and are never canceled, while people’s lanes keep running.
  • Cancel superseded runs: when an agent pushes a new commit to a pull request, Frazer cancels the run for the older commit. People’s runs are canceled only by their workflow’s own concurrency setting.
  • Skip identical trees: when the same tree passed on this pool in the last 24 hours, Frazer posts a check run that links to that result instead of running it again.

Frazer Agents read the lane’s remaining budget and slow their pushes before it runs out. To raise the budget or the cap, apply a plan from the Runners Overview, or update the pool:

Terminal window
frazer githubrunners pools update acme-ci --lane agent.cap=40% --lane agent.budget=1900m --dry-run

Before a scaling change applies, you can replay it against a stored trace of the pool’s queue: the plan on the pool’s Scaling tab shows the wait at the 90th percentile for people and for agents. An excerpt of frazer githubrunners pools get acme-ci -o yaml, with the agent lane and the pool’s scaling:

lanes:
agent:
suffix: -agent
floor: 4
cap: 40%
budget: { runnerMinutesPerDay: 1900, onExhausted: hold }
cancelSuperseded: true
skipIdenticalTree: 24h
backpressure: true
scaling: { min: 2, max: 60, overcommit: 3.0, warm: { mode: learned, windowDays: 14 }, scaleDownIdle: 2m }

A job page builds its phases from three sources: GitHub’s timestamps, the VM’s own agent (claim, boot, warm-disk attach, checkout) and the cache agent (each restore with its tier, bytes and seconds). Tests come from JUnit XML or go test -json. Cost is in runner-seconds, and in dollars on the fleet.

When a job is slower than usual, the page compares its phases and slowest tests with the job’s median over the last 7 days and names causes from records: an eviction, a full Git fetch, a cache refusal, a new test, a cold GPU VM. Nothing is guessed. Explain hands the record to the Assistant.

Terminal window
frazer githubrunners jobs list --project acme-production --slow --since 24h
frazer githubrunners jobs get 8812345 --why

The Insights tab shows jobs by author type (human or agent), queue wait per lane, runner-minutes per merged pull request, the slowest steps with their trend, flaky tests (failed, then passed on the same commit), cache hit rate per repository and fleet spend. A flaky test gets a quarantine pull request that adds it to .frazer/quarantine.yaml.

Terminal window
frazer githubrunners insights --project acme-production --flaky

To reproduce a failure, open a devbox from the failed job’s image, warm disk and environment:

Terminal window
frazer githubrunners jobs debug 8812345

The default image, frazer-ubuntu-24.04, carries the same tool set as GitHub’s hosted runner images (languages, tools, Docker and buildx). It is rebuilt every week and signed.

To build your own, create a runner image from a Dockerfile, a devcontainer.json or a workspace template. A workspace template builds the runner image, the devbox image and the agent sandbox image from the same bytes, so CI, your engineers’ boxes and your agents run the same tools.

Image versions are numbered and roll out in rings: ring 1 is a canary that takes 5 % of jobs, ring 2 is every job once ring 1 has run without new failures. Rollback is one click. The runner binary’s version is part of the image, so runner upgrades roll out the same way.

Terminal window
frazer githubrunners images versions list acme-ci-ubuntu-24.04

Code from a fork runs only on a pool marked untrusted: microVMs on the fleet or on isolated servers, with no secrets, an egress allow-list, read-only caches and no warm-disk commit. GitHub’s approval for outside contributors stays on. Trusted pools refuse workflows that use pull_request_target, because it runs fork code with the base repository’s secrets.

  • Who manages pools: members of the host project with the right roles create and change pools, always through a plan. Sharing a pool with another project needs the runners.runnerPools.share permission and is recorded in both projects.
  • Who sees jobs: members of the project a repository is shared to see its jobs, tests and costs.
  • No long-lived credentials: each VM gets a just-in-time runner registration. Jobs reach Frazer Registry, Frazer Secrets and the cache with the job’s own OIDC token, and secret values are scoped to the job. The App’s private key is a key that can sign but never be exported.
  • Egress you can read: each pool has an egress allow-list (GitHub, Frazer endpoints and the site’s package proxies). A new policy runs in audit mode for 7 days, then enforces. Every destination is logged per job, and internal address ranges and node metadata are always denied.
  • Watchdog: a runner is wedged when a step shows no progress and no output for 5 minutes, its connection to GitHub’s broker is dead, or a step runs past 1.5 times its timeout. Frazer destroys the VM, re-runs the job once and records it; a second wedge on the same job opens an incident.
  • Alerts: every pool ships with Prometheus rules FrazerRunnerWedged, FrazerRunnerQueueP90High, FrazerRunnerBootSlow and FrazerCacheHitDrop.
  • Budget caps: when a project’s budget cap is reached, runner jobs on the Frazer fleet stay queued; nothing is deleted.
Setting Default
Jobs per runner 1; the VM is destroyed after the job
Lane floors Release 8, Main 6, Agent 4 runners
Agent lane cap 40 % of the pool’s maximum
Agent lane budget 1,900 runner-minutes a day per pool; then held
Skip identical trees Same tree passed on the pool within 24 hours
CPU overcommit 3.0 on your own servers; none on the Frazer fleet
Warm pool Learned per hour and weekday; never more than 20 % of the pool’s maximum
Idle warm VMs Destroyed after 2 minutes above the warm target
Overflow to the fleet When a lane other than Batch waits more than 60 s at the 90th percentile, or the pool has no free capacity for 60 s
Wedged runner No progress for 5 minutes, a dead broker connection, or 1.5 × a step’s timeout
Start Queued → in progress p50 ≤ 3 s, p95 ≤ 10 s at every size in a burst of 15 (target)
Organization scope Pools are organization-level runner groups; enterprise-level runners are not supported

On your own servers, GitHub runners and Frazer Cache are part of the per-core server fee, with no per-minute charge. On the Frazer fleet you pay per runner-second by size, with no minimum.

GitHub runnersOn your serversOn the Frazer fleet
PriceIncluded · no per-minute fee$0.045 / vCPU-hour with 4 GiB per vCPU, per second, no minimum, no free minutes · every size below

Every line, with the estimator: pricing.

Symptom Cause Fix
Agent jobs stay queued, marked Held: agent budget The agent lane used its daily budget. Wait for the next day’s budget, or raise it from the Runners Overview or with frazer githubrunners pools update acme-ci --lane agent.budget=… --dry-run.
A job stays queued and no runner picks it up Its runs-on label is not a size and lane of the pool, or the repository is not shared to a project on the pool. Compare the label with frazer githubrunners pools snippet, and add the repository to a project’s share entry.
The create or update plan names a workflow file The workflow uses pull_request_target, which trusted pools refuse. Remove pull_request_target from that workflow, or run it on an untrusted pool.
A fork’s pull request never runs Fork code runs only on untrusted pools. Create an untrusted pool for public repositories.
A job was re-run on its own The watchdog found the runner wedged, destroyed it and re-ran the job. Read the job’s Activity tab. A second wedge on the same job opens an incident.
A job is slower than usual A phase took longer than its 7-day median. Run frazer githubrunners jobs get <job> --why, or open the job page; the causes come from records.
Restores are slow or miss The warm disk was evicted, the toolchain changed, or the cache is degraded. See Cache troubleshooting. A cache outage only makes jobs slower; it never fails them.
A run cannot be canceled through GitHub’s API A zombie run on GitHub’s side. The console lists it with GitHub Support steps and leaves it out of the queue math.

CLI

CommandWhat it does
frazer githubrunners pools createCreate a runner pool from a plan, with its provider, connection, placement, sizes, isolation and cache policy.
frazer githubrunners pools getShow a pool; with -o yaml, its full spec, lanes and scaling included.
frazer githubrunners pools updateChange a pool's lanes, caps, budgets or scaling, as a plan.
frazer githubrunners pools snippetPrint the lane-aware runs-on line for a pool.
frazer githubrunners pools shareShare a pool with other projects and name the repositories each one runs, as a plan.
frazer githubrunners jobs listList a project's jobs, for example only slow ones in the last 24 hours.
frazer githubrunners jobs getShow a job's phases; with --why, compare them with the job's usual time and name the causes.
frazer githubrunners jobs debugOpen a devbox from a failed job's image, warm disk and environment.
frazer githubrunners images versions listList the versions of a runner image and the ring each one is in.
frazer githubrunners insightsShow a project's CI insights, such as flaky tests.

All of them: CLI reference.

MCP tools

ToolWhat it does
frazer.runners.v1Read pools, jobs, phases, tests, insights and a lane's remaining budget; plan pool changes. Writes become a plan that waits for approval.

All of them: MCP tools reference.

  • Cache: the tiers, write rules and quotas behind every job.
  • Devboxes: the same images and caches for your engineers.
  • Agents: agents that open pull requests and read their lane’s budget.
  • Reviewer: reads job results as records.
  • Registry: push images from CI without a stored password.
  • GitHub runners product page