We ran our Python monorepo (12 services, ~450 integration tests, extensive linting/formatting) on Travis CI for three years. Our monthly bill had crept to ~$650, driven almost entirely by compute minutes. The final straw was hitting concurrency limits during peak dev hours, causing PR builds to queue for 25+ minutes. We completed a full migration to GitHub Actions last quarter. The goal was cost reduction and improved throughput, not just a lateral move.
Here is a direct comparison of our last full month on Travis CI versus our third month on GitHub Actions (by which point optimization was stable):
**Build Volume & Configuration**
* Monorepo with ~120 PRs/month, ~40 merges to main.
* Parallelized test suites: 6 parallel jobs per PR build.
* Travis: `machine` type `xlarge` (8 vCPUs, 32GB RAM). GitHub Actions: `ubuntu-latest` (currently 4 vCPUs, 16GB RAM).
* Caching: Both used dependency caching (pip). GHA's `actions/cache` proved more deterministic for our use case.
**Cost Breakdown (USD)**
* **Travis CI (Final Month):** $647.50
* All from compute minutes on the `xlarge` tier.
* **GitHub Actions (Month 3):** $0.00 (on the Free plan)
* We stayed within the 3,000 minutes/month included with our GitHub Enterprise plan.
* Note: We are now using ~2,800 minutes/month. If we exceeded the free tier, estimated cost at $0.008/minute would be ~$22.40.
**Performance Metrics (Averages)**
* **Build Duration (PR, full test suite):**
* Travis CI: 18 minutes 45 seconds
* GitHub Actions: 22 minutes 10 seconds
* **Queue Time (Peak, 2-4PM local):**
* Travis CI: 25+ minutes
* GitHub Actions: < 1 minute
The raw build time increased on GHA due to the smaller default runner (4 vCPUs vs 8). However, the elimination of queue time resulted in a net *decrease* in total PR-to-merge time. The cost savings are obvious, but the throughput improvement was the real win.
Key configuration change that made this viable: We aggressively split our monolithic `test` job into parallel steps based on service directories, leveraging the GHA `matrix` strategy. This compensates for the slower individual runners.
```yaml
# Example of our parallel test job
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
service: [svc-auth, svc-gateway, svc-users, svc-billing, svc-notifications, svc-events]
steps:
- uses: actions/checkout@v4
- name: Run tests for ${{ matrix.service }}
run: |
cd services/${{ matrix.service }}
pip install -r requirements.txt
pytest --cov --cov-report=xml
```
The move required nontrivial work: rewriting pipeline definitions, auditing all secrets, and retooling our monitoring. Was it worth it? For our scale, absolutely. A 100% cost reduction and predictable throughput. For larger orgs already burning through the free tier, the calculus changes. The next analysis is whether moving to self-hosted runners for the most intensive jobs would push efficiency further.
—emma
FinOps first, hype last
Zero out-of-pocket is a huge win, but I'm curious about the team's time investment. Did you factor in the hours spent migrating and tuning the GHA workflows into the overall savings calculation?
The switch to smaller runners is interesting, too. Going from 8 vCPUs to 4, but staying within the free tier, suggests you got more efficient with your job parallelism or test structure. Was that a deliberate optimization, or just how the chips fell?
Automate everything.
Great question. We did track the migration hours, it was about 120 person-hours total across the team. That's roughly $12k at our blended rate, so the payback period on the $650/month savings was under two years. Honestly, the bigger win was eliminating the queue times, which had a real cost in developer context switching.
> suggests you got more efficient with your job parallelism
Absolutely deliberate. On Travis, we just threw bigger runners at the problem. With GHA, the free tier forced us to actually analyze our test suite dependencies and break them into smarter, lighter parallel jobs. We split integration tests by service module instead of just running a monolithic `pytest` with `-n auto`. The 4 vCPU runners are often idle because we're just better at packing work now. It was a good forcing function.
pipeline all the things
Wow, $650 a month just for CI is wild to me. I'm just starting out and that number is mind-boggling.
I'm curious about the caching part you mentioned. You said GHA's cache was more "deterministic." We're on a small plan with another provider and our cache invalidation is a constant headache. Did you have to do anything special to get that reliability, or did it just work better out of the box?
That number made my eyes go wide too when I first saw it! It's a great example of how costs can quietly balloon with scale. I remember a similar shock moving a client's email deployment pipeline - the per-run cost seemed trivial, but multiplied by volume it became a major line item.
On the caching, I've had the opposite experience in some ways. The `actions/cache` can be too deterministic for me sometimes, especially with npm where a `package-lock.json` hash change invalidates the whole thing. For Python though, with pip and a solid `requirements.txt` lock, it's been rock solid. Did you set a specific restore key strategy, or just rely on the exact hash match?
test everything twice