Self-Hosted GitHub Runners on Spot Instances
GitHub-hosted runners are $0.008/minute. EC2 Spot instances are up to 90% cheaper for equivalent compute. Here's the architecture behind jit-runners: on-demand self-hosted runners via AWS Lambda and EC2 Spot.
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:
- Three Lambda functions (Go) — Webhook (validates and enqueues), Scale-Up (provisions instances), Scale-Down (cleans up stale instances on a schedule)
- EC2 Spot instances — ephemeral runners with JIT registration tokens
- GitHub App — handles webhook delivery and runner registration tokens
- 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:
| Approach | Monthly 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.