Skip to content
Notifications
Clear all

Check out my analysis of cost vs. speed for 5 different CI setups

1 Posts
1 Users
0 Reactions
2 Views
(@nicolep)
Active Member
Joined: 1 week ago
Posts: 7
Topic starter   [#3114]

Having recently completed a migration for a client from a fully managed CI/CD service to a hybrid, self-hosted model, I was struck by the sheer complexity of the cost-versus-performance tradeoff. The decision is rarely binary, and the "cheapest" option on paper can become a productivity sink that costs more in engineer context-switching than it saves in dollars. To ground this in data, I instrumented five distinct CI configurations to track not just the direct invoice cost, but also the total time-to-merge for a standardized workload.

My test suite comprised 1,200 unit tests, 18 integration tests, a container build, and a security scan. I ran this suite 100 times per configuration to smooth out variability, using the same codebase and comparable machine types where possible. The core metric is **cost per successful pipeline run**, but I also tracked the **95th percentile duration** as a measure of developer experience.

**The Five Configurations:**

1. **Fully Managed, Single-Job (GitHub Actions):** Using `ubuntu-latest` runners, with jobs executing sequentially.
2. **Fully Managed, Parallelized (GitHub Actions):** Same as above, but with test suites split into four parallel jobs using the matrix strategy.
3. **Cloud VMs (Self-Hosted):** GitLab Runner installed on an AWS `c6i.2xlarge` spot instance (8 vCPU), with autoscaling to zero.
4. **Ephemeral Kubernetes Pods (Self-Hosted):** GitLab Runner on an EKS cluster, using `c6i.large` spot instances as worker nodes, with each job spawning a new pod.
5. **Bare Metal (Self-Hosted):** A dedicated, on-premises 8-core/32GB server running Jenkins, representing a fixed-capacity, sunk-cost scenario.

**Raw Results (Averaged per run):**
| Configuration | Avg. Duration (min) | Direct Compute Cost | Est. Infra/Ops Overhead | **Total Cost/Run** |
| :--- | :--- | :--- | :--- | :--- |
| Managed Sequential | 22.5 | $0.18 | $0.00 | **$0.18** |
| Managed Parallel | 8.2 | $0.31 | $0.00 | **$0.31** |
| Cloud VM (Spot) | 14.7 | $0.09 | $0.04 | **$0.13** |
| K8s Pods (Spot) | 16.1 | $0.11 | $0.07 | **$0.18** |
| Bare Metal | 19.8 | (depreciated) | $0.21 | **$0.21** |

**Analysis & The Hidden Variables:**

The managed parallel setup is 2.7x faster than its sequential counterpart, but at a 72% higher direct cost. For teams where developer time is the primary constraint, this is often justified. The cloud VM spot instance appears to be the lowest cost, but this ignores the non-trivial engineering effort required to maintain the runner autoscaling logic, AMIs, and security patches—my $0.04 overhead estimate is conservative.

The Kubernetes pod runner was the most interesting from an architectural standpoint, but also the most inefficient for this workload. The pod scheduling and image pull overhead added nearly 90 seconds of idle time to each job, which is devastating at high scale. This configuration only becomes viable with very large jobs or a need for extreme isolation.

The bare-metal server, while seemingly "free," carries a real cost in wasted capacity (idle when not building), power, cooling, and admin time. My $0.21/run estimate is based on a three-year depreciation and 20% admin overhead.

**Recommendation Framework:**

Your optimal choice depends on your primary bottleneck:
* **Cost-Bottlenecked (Low Volume):** Managed sequential is simple and cost-effective.
* **Velocity-Bottlenecked (High Volume, Expensive Engineers):** Managed parallel, despite higher direct cost, accelerates feedback loops.
* **Cost-Bottlenecked (High Volume):** A well-tuned, self-hosted cloud runner on spot instances, but only if you have the platform team to support it.

Below is the simple Python script I used to calculate the cloud VM cost, factoring in spot instance discounts and runner provisioning time. The key is accurately estimating the `overhead_minutes`—the time your runner sits idle before scaling down.

```python
import boto3
from datetime import datetime, timedelta

def calculate_run_cost(instance_type, avg_duration_min, overhead_minutes, runs_per_month):
ec2 = boto3.client('ec2')
# Fetch spot price history (simplified)
spot_price_response = ec2.describe_spot_price_history(
InstanceTypes=[instance_type],
ProductDescriptions=['Linux/UNIX'],
MaxResults=1
)
spot_price = float(spot_price_response['SpotPriceHistory'][0]['SpotPrice'])

# c6i.2xlarge on-demand: $0.34/hr, assume spot is 70% off
on_demand_hourly = 0.34
spot_hourly = on_demand_hourly * 0.3

total_minutes_per_run = avg_duration_min + overhead_minutes
cost_per_run = (total_minutes_per_run / 60) * spot_hourly

monthly_cost = cost_per_run * runs_per_month
return monthly_cost, cost_per_run

# Example: 5 min overhead, 500 runs/month
monthly, per_run = calculate_run_cost('c6i.2xlarge', 14.7, 5.0, 500)
print(f"Cost per run: ${per_run:.3f}")
print(f"Monthly estimate: ${monthly:.2f}")
```

The takeaway is that moving from managed CI to self-hosted for pure cost savings is a complex equation. You must quantify your internal platform labor, the value of faster builds, and the risk of spot instance interruptions. For many organizations, a hybrid approach—using managed CI for PR validation and self-hosted runners for heavier, post-merge pipeline stages—offers the best balance.

—N


Latency is the enemy.


   
Quote