GitHub Actions Interview Questions

Self-Hosted Runners and Auto-Scaling

questions
Scroll to track progress

Your startup's CI/CD pipeline is running 200+ jobs daily, but GitHub-hosted runners are hitting concurrent job limits during peak development hours. You're losing 2-3 hours of deployment velocity every morning. The team wants to switch to self-hosted runners but is worried about infrastructure overhead.

Self-hosted runners solve concurrency constraints by letting you provision unlimited machines. You'd register runners with GitHub using the registration token, configure them to scale with your workload (using orchestration like Kubernetes or cloud auto-scaling), and route high-priority jobs to dedicated runners. Key benefits: unlimited concurrency, lower per-job costs after initial investment, and control over runner specs (CPU, GPU, memory). Costs shift from GitHub billing to your infrastructure spend—typically 40-60% cheaper at scale. You'd implement health checks, automatic de-registration on failure, and ephemeral runners (spawn for a job, destroy after) to prevent resource leaks.

Follow-up: How would you prevent a runaway workflow from consuming all self-hosted runner capacity and blocking other teams?

Your team deployed 5 self-hosted runners, but after a surge in commits, all runners are "offline" and jobs are queued indefinitely. The CI system shows runners as "idle" in the GitHub UI, but they're not picking up new jobs. You have 90 minutes to restore CI/CD without losing historical data.

This is a registration/connectivity issue. First, SSH into a runner machine and check the runner service status (`systemctl status actions-runner` on Linux, or check Services on Windows). Common causes: the runner lost connection to GitHub (network/firewall), the registration token expired (re-register with a new token from repo settings), or the runner process crashed. Run `/svc.sh status` (Linux) to verify the service is running. If runners show "offline," remove them from GitHub's UI and re-register with fresh tokens. For rapid recovery, implement a health check script that restarts the runner service if it stops—avoid manual restarts. In future, containerize runners (Docker or Kubernetes) so you can quickly redeploy on failure.

Follow-up: How would you structure a Kubernetes-based self-hosted runner setup to auto-scale based on GitHub queue depth?

You're running self-hosted runners on-premises in a data center with 8 physical machines. During a major release, your team kicks off a load test workflow that pegs all 8 runners to 100% CPU for 4 hours. The database backup job, which runs every night, is completely starved and misses its SLA window for the first time in 2 years.

This is a lack of runner prioritization and isolation. You need to segment runners by workload: create labels (e.g., `load-test`, `production-critical`, `general`) and assign runners accordingly. The database backup should run on dedicated runners with `runs-on: [self-hosted, production-critical]`. Load tests route to `runs-on: [self-hosted, load-test]` with resource limits (CPU, memory concurrency). Implement a runner group hierarchy: reserve 2-3 machines exclusively for critical jobs, 3-4 for general use, and scale ephemeral runners for bursts. Monitor runner utilization and set up job queuing rules so high-priority jobs get first access. For on-premises setups, pair this with Linux cgroups or container resource limits to prevent a single job from consuming all CPU.

Follow-up: How would you guarantee a critical job always has a runner available within 5 minutes?

Your organization has 150 engineers across 3 time zones, each pushing multiple times a day. Self-hosted runners are now at 85% utilization on average, with spikes to 100%. You're considering scaling from 20 to 40 runners, but the CFO wants to understand ROI: "Is it cheaper than just paying GitHub?"

Model both scenarios: GitHub hosted runners cost ~$0.008 per minute for standard runners, ~$0.016 for larger runners. At 150 engineers × 5 builds/day × 10 min/build = 12,500 runner-minutes/day × $0.008 = ~$100/day or ~$36K/year. Self-hosted: 40 machines × $500/month (cloud VM rental) = $20K/year, plus operational overhead (monitoring, maintenance, security patches ~$10K/year). Self-hosted breaks even ~10 months in, then saves $16K/year. However, factor in: uptime risk (GitHub handles HA), security/compliance requirements (your network isolation), and engineering time. For 150 engineers, recommend hybrid: self-hosted for predictable load (CI/CD, unit tests), GitHub-hosted for spikes (integration tests, large matrix builds). This caps self-hosted spend while staying flexible.

Follow-up: How would you monitor runner utilization to trigger auto-scaling at 75% capacity?

You've deployed self-hosted runners on EC2 instances with auto-scaling groups. A workflow triggers an EC2 instance to launch, but the runner never registers with GitHub—jobs timeout waiting for a runner. Logs show the instance is running, but GitHub still shows 0 idle runners for that label.

This is a runner registration lag or bootstrap script failure. The EC2 instance's user data script must: download the latest runner agent, extract it, generate a registration token from GitHub (using a service token or API), and execute the registration. Common issues: registration token is expired (valid for 1 hour), the user data script failed silently (check CloudWatch logs), the instance doesn't have outbound internet access, or the runner service didn't start. To debug, SSH into the instance and manually run `/config.sh --url https://github.com/[org]/[repo] --token [token]`. For production, wrap the runner agent in a container (Docker + systemd) so you can quickly redeploy. Add health checks: the runner should send a heartbeat to GitHub every 60s—if it stops, EC2 auto-scaling tears down and respawns the instance.

Follow-up: Design a self-healing runner pool where instances automatically replace themselves if they miss 3 consecutive health checks.

Your organization stores sensitive build artifacts (private keys, API tokens) on self-hosted runners. An engineer accidentally commits a GitHub Actions workflow that logs the entire runner's `/tmp` directory in the job output, exposing secrets to everyone with access to the Actions log. You need to rotate all secrets and prevent this from happening again.

Immediate: rotate all exposed secrets, revoke tokens, change SSH keys. Then implement hardening: (1) run workflows in containers/VMs that self-destruct after each job—no persistent secrets on the runner filesystem. (2) Use GitHub's `actions/checkout@v4` with credential stripping; secrets are masked in logs but can leak via step output if explicitly echoed. (3) Enforce a policy: no direct secret storage on runners; use HashiCorp Vault or AWS Secrets Manager, fetched via OIDC token exchange at runtime. (4) Audit runner environment: mount `/tmp` as tmpfs (in-memory, auto-cleared), set restrictive file permissions, disable job logs for high-risk workflows. (5) Implement RBAC: only senior engineers can view runner logs. For ephemeral runners (preferred), spin up a fresh instance per job—no persistent state to leak.

Follow-up: How would you implement OIDC-based secret injection so runners never store credentials on disk?

You have 3 self-hosted runners in your primary data center. During a network outage (all 3 runners lose connectivity for 2 minutes), jobs start failing with "runner not available" errors. Engineers assume the outage is permanent and manually trigger rollbacks, causing 1 hour of service disruption. GitHub Actions didn't wait for runner reconnection.

GitHub queues jobs for ~35 minutes before timing out. A 2-minute network blip should auto-recover if the runner reconnects. The issue: your workflow was already running and the job was forcefully cancelled (not queued). For resilience, implement: (1) runner redundancy across multiple data centers or AZs—never rely on a single DC. (2) Use a "circuit breaker" pattern: if a runner group loses all machines, workflows fall back to GitHub-hosted runners automatically. (3) Add retry logic in workflows: `retry: 2` on critical jobs. (4) Implement runbook automation: if all self-hosted runners are offline for >5 minutes, automatically route to GitHub-hosted runners via conditional job routing. (5) Set up passive monitoring: GitHub Actions API exposes runner status; poll it from your monitoring system and alert on group-level failures before users notice.

Follow-up: Design a workflow that gracefully degrades to GitHub-hosted runners if self-hosted runners are unavailable for >10 minutes.

Your team is scaling to 50 self-hosted runners across 2 data centers. You need to deploy a new version of the runner agent (security patch) to all 50 machines without disrupting active CI/CD jobs. A coordinated shutdown could queue 100+ jobs, delaying deployments by 30+ minutes.

Use a rolling update pattern: mark runners as "offline" in groups (e.g., 10% at a time), wait for existing jobs to finish, update the runner agent and OS, then bring runners back online. Orchestrate this with a script that calls GitHub's REST API to list runners, tags them by data center, and batches updates. For zero-downtime updates: use container-based runners (Kubernetes, Docker swarm); update the container image, drain the node (no new jobs), let existing jobs complete, then respawn with the new image. Set a grace period (e.g., 5 minutes) for running jobs—if a job is still running after grace, forcefully stop it and re-queue it. For critical infrastructure, implement canary updates: update 1 runner, run a test workflow on it, verify it works, then roll out to the rest.

Follow-up: How would you automate this rolling update process using GitHub's API and a custom orchestration script?

Want to go deeper?