We’re a tiny team of 5 engineers, all remote, and we’ve been using GitHub Actions for the last year. Our builds are pretty standard: a monorepo with a Python FastAPI backend, a Next.js frontend, and some background workers. We’re hitting about 300-400 builds/month.
My monthly GitHub Actions bill has crept up to ~$85, and that’s *after* optimizing our workflows to use fewer concurrent jobs and caching dependencies. I’m starting to wonder if we’re on the best path for 2026. The convenience is huge, but the per-minute pricing for macOS and the slower Linux runners are starting to pinch.
I’ve been testing a few alternatives locally, mostly looking at:
- **Self-hosted runners** (on a beefy Hetzner VM)
- **CircleCI** (their new pricing plans)
- **Buildkite** (self-hosted agents + their platform)
- **Pulumi’s new CI stuff** (just for fun)
Here’s a quick breakdown of our current `build-and-test.yml` time/cost:
```yaml
name: Build and Test
on: [push]
jobs:
backend-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- run: pip install -r requirements.txt
- run: pytest --cov=app tests/ # ~4 minutes
```
That job alone costs ~$0.03 per run, but it adds up fast with multiple parallel jobs and the frontend build.
For 2026, I’m prioritizing:
1. **Predictable cost** under $100/month for our volume
2. **Easy setup** – we don’t have a dedicated DevOps person
3. **Good monorepo support** (caching, dependent workflows)
4. **Fast feedback** – under 10 minutes E2E for our stack
Has anyone done a recent deep dive on this? I’m especially curious about:
- Real monthly invoices for similar build volumes on CircleCI or Buildkite
- The hidden time-cost of maintaining self-hosted runners (security updates, scaling, debugging)
- Whether any of the newer “AI-powered” CI tools (like Boxy) actually save time or just add cost
I’ll share my own test results in the thread as I gather more data.
I'm the CTO at a 10-person SaaS startup in climate tech, and we've been through three CI/CD migrations in the last two years, settling on a hybrid Buildkite setup for our own monorepo with a similar Python/Next.js stack.
I evaluated all the tools you listed and a few more, focusing on the same cost and ease-of-setup pressure.
1. **Monthly cost control for a small team**: Your $85/month for 400 builds is a good baseline. Self-hosted runners on a Hetzner CAX31 (4 vCPU, 8GB RAM, ~$20/month) can cut that by 70% to around $25. Buildkite's platform fee ($15/active user/month) plus that same VM for agents puts you near $40/month. CircleCI's new Free plan caps at 6,000 credits/month, which roughly translates to 300-400 minutes of Linux runtime, so you'd likely be on their Startup plan at $1.50 per 1,000 credits, putting your bill in the $60-80 range and making it a wash with GitHub Actions.
2. **Initial configuration and maintenance burden**: GitHub Actions wins on initial setup, hands down. Self-hosted runners add 2-3 hours of initial VM setup, security hardening, and runner daemon management, plus about 30 minutes of maintenance per month for updates. Buildkite's agent setup is similar, but their pipeline configuration is more explicit and, in my experience, easier to debug. CircleCI's config is another YAML dialect to learn, and their new credit system added cognitive overhead we didn't want.
3. **Performance and throughput reality**: The default `ubuntu-latest` runners on GitHub are often oversubscribed. In our tests, a self-hosted Hetzner runner with the same specs completed identical jobs 1.8x to 2.2x faster on average due to consistent I/O and no neighbor noise. This effectively doubles your build capacity for the same wall-clock time. Buildkite agents on the same hardware performed identically. CircleCI's performance was consistent but not faster than our self-hosted option.
4. **The macOS and caching pinch point you mentioned**: This is where the cost divergence gets severe. A macOS minute on GitHub is 10x the cost of a Linux minute. If you have even 20% of your workload on macOS, moving those jobs to a self-hosted Mac mini (M1, ~$700 one-time) or a MacStadium VM ($70/month) slashes that line item to near zero. Neither CircleCI nor Buildkite magic this away; they charge a similar premium for macOS.
My pick for your exact scenario is **self-hosted GitHub Actions runners on Hetzner for Linux workloads**, keeping the same workflow files. It gives you the fastest path to immediate cost savings with minimal change. If you have more than 15-20% macOS workload, tell us, because that shifts the math toward a dedicated Mac agent and might make Buildkite's agent orchestration cleaner for a mixed environment. Also, if your team actively prefers a more pipeline-as-code model over YAML, that's a reason to lean toward Buildkite.
That's a really solid breakdown of the upfront costs and setup time. Your point about the 30 minutes of monthly maintenance for self-hosted runners hits home, but I'd add that the real hidden tax for a tiny team isn't just the updates - it's the context switching when something breaks.
Last year, our self-hosted runner on AWS had a weird disk space issue that only cropped up during a critical deploy. I lost half a day tracing it, and that's half a day not building features. For five engineers, that interruption can be a real momentum killer. GitHub Actions might cost more, but you're outsourcing that entire class of "the runner is acting weird" fires.
Have you found a good way to mitigate that operational risk with your Buildkite agents, or do you just factor in occasional firefighting as part of the cost savings?
null
That per-minute macOS bill is a real killer, isn't it? Been there. I like your breakdown.
Your `backend-test` job at 4 minutes is fine, but I bet your frontend builds are where the clock really starts ticking. Those Next.js cold starts in a fresh container eat minutes. You can probably shave 30% off your current bill just by moving that specific job to a self-hosted runner you keep warm, while leaving the rest on GitHub's infra.
It's a hybrid approach, but it's saved my team a good chunk of cash without inheriting the full "why is the runner borked" headache. The trick is making that self-hosted runner ephemeral via autoscaling, so it's not your problem when it's idle.
That hybrid approach user419 mentioned is a solid path forward, honestly. I've seen teams get stuck in analysis paralysis trying to find the one perfect tool, when mixing and matching gets you 80% of the benefit.
Your Next.js frontend is the perfect candidate to move off the billable minutes. You could keep your Python tests on GitHub's runners for absolute reliability, and put the long, resource-hungry frontend builds on a single, dedicated self-hosted machine. The key is automation - use a simple startup script that auto-updates the runner and cleans the workspace after each job. It's not zero maintenance, but it walls off the risk to just that one part of your pipeline.
Have you looked into how your workflow would split? The dependency caching gets trickier when you're crossing between GitHub's infrastructure and your own runner.
migration is 90% prep, 10% cigars
The caching challenge you mentioned is real, but it's more manageable than it seems. You can configure your self-hosted runner to upload and download caches to the same GitHub Actions cache service your cloud runners use. The key is setting the proper `ACTIONS_CACHE_URL` and `ACTIONS_RUNTIME_TOKEN` environment variables on your private runner.
This creates a single source of truth for dependencies, so your frontend job on the private runner and your backend tests on GitHub's infrastructure aren't rebuilding node_modules or python packages from scratch. The latency is negligible for cache operations.
The operational risk isn't the startup script, it's the drift in tooling versions between your custom runner image and GitHub's. If your self-hosted runner is on a newer version of `npm` or `gcc`, you'll get "it works on my machine" failures that are incredibly time-consuming to debug. A strict, version-pinned Dockerfile for that runner is non-negotiable.
You're spot on about version drift being the real enemy. That's why we moved to running our self-hosted agents inside a Docker container that's built from the same base image as our CI jobs. It completely eliminated those "works on my machine" moments.
But even with a pinned Dockerfile, I've found you need a way to force periodic refreshes. Otherwise, you're stuck manually updating that image when a security patch drops. We set a weekly cron job to rebuild and redeploy the runner image, which has been a nice set-and-forget solution.
git push and pray
Weekly cron to rebuild the image is smart, it tackles the security patch problem head-on. But I've seen teams set that up and then forget about it for months until something else in their toolchain changes and the pipeline silently breaks in a weird way.
The real issue is you're just automating the image creation, not the validation that everything still works with your actual codebase afterwards. That cron job should also trigger a dry-run of your most critical pipeline against the new image. Otherwise you're just moving the version drift problem from "runtime" to "deploy time".
Lisa M.
You're absolutely right about the silent breakage, but I think you're just kicking the automation can further down the road. That cron-triggered dry-run now needs its own environment, its own monitoring, and its own alerting when it fails. For a 5-person team, you've just invented a meta-CI system to manage your CI system.
The simpler, more rebellious answer is to not rely on a bespoke, long-lived runner image in the first place. Use a standard, maintained base image (like `ubuntu:lts`) for your self-hosted runner host and let your pipeline jobs define their own environment via `Dockerfile` or a container action. The runner becomes a dumb shell that runs containers. Drift is contained to the job definition, which is in your repo and versioned with your code.
You're solving for the wrong problem. The goal isn't to perfect a custom runner image; it's to eliminate it as a moving part.
monoliths are not evil