GitHub-hosted runners cost $0.008 per minute (Linux, 2 vCPU / 7 GB). If your team runs 500 minutes of CI per day, that’s $1,200/month. For a startup with a moderately active engineering team, CI bills are often the largest single cloud cost line item.

The standard alternative — persistent self-hosted runners — solves the cost problem but introduces uptime management: servers that are always on, patching to handle, capacity to predict. On bursty workloads (PR-heavy development, release days), they’re either under-provisioned (queued builds) or over-provisioned (idle machines you’re paying for).

jit-runners is our answer: GitHub Actions runners that provision on demand when a workflow starts, use EC2 Spot for 60–80% cost reduction versus on-demand, and terminate when the job finishes.

The architecture

GitHub Actions workflow queued


GitHub webhook (workflow_job: queued)


API Gateway → Webhook Lambda (Go)

        ├── Validates HMAC signature, enqueues to SQS (30s delay for dedup)


SQS → Scale-Up Lambda (Go)

        ├── Generates JIT runner token via GitHub API
        ├── EC2 RunInstances (Spot, user data: runner install + register)


EC2 instance comes online

        ├── Registers with GitHub as ephemeral JIT runner


GitHub assigns job to runner

        ├── Job executes
        ├── Runner self-deregisters and instance self-terminates


EventBridge (every 5min) → Scale-Down Lambda (Go)

        ├── Cleans up stale/orphaned instances via DynamoDB state

Four components:

  1. Three Lambda functions (Go)Webhook (validates and enqueues), Scale-Up (provisions instances), Scale-Down (cleans up stale instances on a schedule)
  2. EC2 Spot instances — ephemeral runners with JIT registration tokens
  3. GitHub App — handles webhook delivery and runner registration tokens
  4. Supporting infrastructure — SQS queue, DynamoDB table (runner state), EventBridge scheduler

Why Lambda + Go

Lambda is the right compute for this use case:

  • Event-driven — webhook fires, Lambda runs, done. No polling, no scheduler.
  • Cost — Lambda invocations for webhook handling are essentially free (well within the free tier)
  • Scale — 1,000 concurrent PRs? Lambda scales horizontally without configuration

Go is the right language for this Lambda:

  • Cold start — compiled Go binaries have 10–50ms Lambda cold starts. Python or Node.js are fine too, but Go is genuinely fast.
  • Single binary — no runtime dependencies, trivial deployment (GOARCH=amd64 GOOS=linux go build)
  • AWS SDK v2 — the official Go SDK is well-maintained and performant

The Lambda functions share internal libraries (lambda/internal/) for common operations like GitHub API calls, EC2 provisioning, and DynamoDB state management. Each function has a focused responsibility: webhook validation, instance provisioning, or cleanup.

EC2 Spot for runners

Spot instances offer unused EC2 capacity at 60–80% discount versus on-demand. The risk: AWS can reclaim capacity with 2 minutes notice.

For CI runners, this risk is manageable:

  • Spot interruption = job failure = job retry — GitHub Actions retries interrupted jobs automatically (with retry-failed-jobs: true) or manually
  • Short job duration — most CI jobs finish in 5–15 minutes. Spot interruption probability over that window is low.
  • Diversified instance types — using a Spot Fleet or instance type diversification reduces interruption frequency

The default instance type when no label matches is t3.large. You can configure instance types per workflow label (e.g. runs-on: [self-hosted, c6i.4xlarge]). With Spot instances, CI costs drop dramatically compared to on-demand pricing.

The user data script

When the EC2 instance starts, user data handles runner setup:

#!/bin/bash
set -euo pipefail

# Install runner
mkdir -p /actions-runner && cd /actions-runner
curl -sSL https://github.com/actions/runner/releases/latest/download/actions-runner-linux-x64-*.tar.gz | tar -xz

# Register as ephemeral runner
./config.sh \
  --url "https://github.com/ORG_NAME" \
  --token "REGISTRATION_TOKEN" \
  --name "spot-$(ec2-metadata --instance-id)" \
  --labels "self-hosted,linux,spot" \
  --ephemeral \
  --unattended

# Run (exits after one job when --ephemeral)
./run.sh

The --ephemeral flag is key: the runner deregisters automatically after completing one job. No cleanup needed, no state to manage between runs.

The registration token (REGISTRATION_TOKEN) is fetched by the Lambda function via the GitHub API just before calling RunInstances, then injected into user data. Tokens expire after one hour — tight enough window for a newly provisioned instance.

Handling Spot interruptions and cleanup

Ephemeral runners (--ephemeral flag) self-deregister from GitHub and self-terminate after completing one job. This handles the normal lifecycle automatically — no cleanup Lambda needed for the happy path.

For abnormal cases (Spot interruptions, failed starts, orphaned instances), the Scale-Down Lambda runs every 5 minutes via EventBridge. It queries DynamoDB for runner state and terminates any instances that are stale, orphaned, or whose runners have already deregistered.

If a Spot interruption kills an instance mid-job, the GitHub Actions job fails and can be retried (manually or via workflow retry-failed-jobs). A new workflow_job: queued event fires for the retried job, provisioning a fresh instance through the normal flow.

jit-runners also falls back to on-demand instances automatically if Spot capacity is unavailable, reducing the risk of queued builds during capacity shortages.

Cost comparison

For a team running 1,000 CI minutes/day:

ApproachMonthly cost
GitHub-hosted runners$240
EC2 on-demand (c5.2xlarge)~$180
EC2 Spot (c5.2xlarge, ~70% discount)~$55

At scale the savings compound. 5,000 minutes/day: GitHub → $1,200/month, Spot → ~$275/month.

The break-even on engineering time to set up jit-runners is typically under a week of CI spend.

What jit-runners handles

The open source jit-runners project provides:

  • Three Lambda functions (Go) for webhook handling, instance provisioning, and cleanup
  • Terraform/OpenTofu modules and CloudFormation template for all AWS resources (Lambda, API Gateway, SQS, DynamoDB, IAM, security groups)
  • Pre-baked Amazon Linux 2023 AMI with an ubuntu-latest-like toolchain (Docker, Node.js, Go, Python, AWS CLI, kubectl) built with Packer
  • GitHub App configuration guide

Both Terraform and CloudFormation deployment guides are in the repository. Setup takes about 30 minutes if you have AWS credentials and GitHub App access.


jit-runners is open source under the Apache License 2.0. Issues and contributions welcome at github.com/devopsfactory-io/jit-runners.