We benchmarked GitLab.com shared runners against our managed Hetzner runners. Here's the data.
Real wall-clock and per-job timings from a 13-job Symfony pipeline on GitLab.com shared vs Runsetters managed runners (€19–79/mo). One small box loses on wall-clock; two boxes reach parity. Plus the cost math that decides which you should be on — and what we found when we tested dedicated hardware.
By Runsetters
If you're running CI on GitLab.com's shared runners and you've ever Googled "GitLab runner alternative", the pitch you hear is some version of: "Our runners are faster." It's the natural sales line, and since we build managed GitLab runners for a living, we'd love for that to be the whole story.
It's not — at least not in the simple way. Here's what we actually saw when we ran the same real pipeline several ways.
TL;DR: GitLab.com shared finished a 13-job Symfony pipeline in 4:15 wall-clock. A single small Runsetters box can't match that — shared quietly fans your pipeline out across ~13 instances, one per job. But two boxes can: our €49/mo Pro tier bought twice (€98/mo, 16 parallel slots) ran the same pipeline in 4:00 warm — a hair faster than shared — and did 12% less total work-time per job. The real reasons to switch aren't raw speed: flat predictable cost (your bill stops climbing as your team grows), and self-hosted GitLab support (which can't use SaaS shared runners at all). For any team past a few hundred pipelines/month, managed runners save real money. For a solo dev with a parallel-heavy pipeline, GitLab.com shared is still a great fit.
The setup
Same pipeline. Same code. Different runner configurations, selected by the .gitlab-ci.yml tags: directive.
The pipeline is a real Symfony app (we'll call it "project-abc" for confidentiality) with 13 CI jobs: composer install, composer audit, php lint, phpunit unit, phpunit functional split into 4 parallel jobs, phpstan, php-cs-fixer, 3 symfony lint variants, and doctrine schema/mapping validation. MySQL 8 + Redis service containers. A typical mid-sized PHP CI pipeline.
Our current tiers run on Hetzner's CX line (shared, cost-optimized vCPU) — that's what keeps the prices where they are:
| Tier | Hardware | vCPU | concurrent slots |
Price |
|---|---|---|---|---|
| Starter | cx23 |
2 | 2 | €19/mo |
| Standard | cx33 |
4 | 4 | €29/mo |
| Pro | cx43 |
8 | 8 | €49/mo |
| Heavy | cx53 |
16 | 16 | €79/mo |
| Beast | (custom) | 32+ | 24+ | Contact sales |
The headline comparison is GitLab.com shared vs 2× Pro — two cx43 boxes registered to the same project under one subscription, using our multi-runner parallelism toggle. Both Pro boxes are real customer-flow provisions (registered an account, paid through Paddle, boxes spun up automatically). This is exactly what a paying customer gets.
Each managed config ran twice — cold (first job pulls the Docker image) and warm (image cached on the box). The shared baseline ran once; every shared job is its own fresh ephemeral instance, so "warm" doesn't apply.
Methodology note, up front: the
mastercommit we benchmarked was red — a Doctrine schema/mapping desync madedoctrine:schema,composer:audit, and the fourphpunit:functionaljobs fail. Critically, they fail identically on GitLab.com shared and on our runners — it's the codebase, not the hardware. So this is a fair comparison of wall-clock and per-job duration for identical work, not a pass/fail benchmark. Failing jobs are marked † below; their durations are still directly comparable.
The data
Wall-clock (what your CI dashboard shows)
| Configuration | Wall-clock | vs shared |
|---|---|---|
| GitLab.com shared | 4:15 (255s) | — |
| 2× Pro — warm (cx43 ×2, 16 slots) | 4:00 (240s) | 0.94× — slightly faster ✅ |
| 2× Pro — cold | 7:27 (447s) | 1.75× slower |
Two cx43 boxes reach wall-clock parity with shared once the image is cached — and edge ahead. The cold run is the one real penalty (more on that below).
Per-job durations (warm, where applicable)
| Job | Shared | 2× Pro warm |
|---|---|---|
| composer:audit † | 27s | 18s |
| composer:dev | 41s | 26s ✅ |
| doctrine:schema † | 214s | 187s |
| doctrine:validate:mapping | 180s | 174s |
| php:lint | 45s | 56s |
| phpcsfixer | 127s | 153s |
| phpstan | 179s | 82s ✅ |
| phpunit:functional 1/4 † | 202s | 169s ✅ |
| phpunit:functional 2/4 † | 199s | 166s ✅ |
| phpunit:functional 3/4 † | 199s | 194s |
| phpunit:functional 4/4 † | 202s | 164s ✅ |
| phpunit:unit | 79s | 91s |
| symfony:lint:container | 204s | 209s |
| symfony:lint:warmup | 208s | 174s ✅ |
| symfony:lint:yaml | 132s | 107s |
| Per-job sum | 2238s | 1970s (−12%) |
† fails on this WIP commit — identically on shared and on our runners (code-side, not the runner).
The per-job sum on 2× Pro is 12% lower than shared. The standout is phpstan: 179s → 82s (−54%). Static analysis is single-threaded and CPU-bound, and an 8-vCPU box with headroom crushes it; a 1-vCPU shared instance can't. (This is the reverse of what we saw on the 2-vCPU Starter in an earlier run, where phpstan was slower — cores matter for this job specifically.)
Cold vs warm (the one real penalty)
| Job | 2× Pro cold → warm |
|---|---|
composer:dev (first job to pull php:74) |
55s → 26s (−53%) |
| All other 12 jobs | within noise once the image is cached |
The cold wall-clock (7:27 vs warm 4:00) is dominated by two fresh boxes each pulling the private php:74 image over the network. GitLab.com shared keeps base images warm on its own infra, so it never pays this. After your first pipeline, our boxes have the image cached and the penalty is gone.
So what's actually going on
The wall-clock story is not "shared is faster than managed." It's "shared fans your pipeline out across ~13 instances for free; one managed box gives you a fixed number of slots."
Wall-clock ≈ (per-job time) × (job count ÷ parallel slots)
GitLab.com shared isn't one runner — it's a fleet, and your pipeline grabs as many instances as it has jobs. 13 jobs → up to 13 parallel runners → wall-clock ≈ the longest job plus a little queue tax.
A managed box gives you a fixed slot count (concurrent in config.toml). One box, N slots:
| Config | Slots | 13 jobs run as… |
|---|---|---|
| Starter (€19) | 2 | ~7 sequential pairs |
| Standard (€29) | 4 | ~4 groups of 4 |
| Pro (€49) | 8 | ~2 groups of 8 |
| 2× Pro (€98) | 16 | all 13 at once → wall-clock ≈ longest job |
| Heavy (€79) | 16 | all 13 at once |
This is why a single small box loses and two boxes win: parallelism is bought in slots, and 16 slots clears 13 jobs in one wave — the same shape as shared. Buying a bigger/second box doesn't make your job faster; it makes more of your jobs run at once.
The cost math — where managed wins
GitLab.com SaaS shared runners bill $0.008 per CI minute (≈ €0.0074/min) — CI minutes, not wall-clock minutes, summed across every job. This pipeline burns ~35 CI minutes, so €0.26 per run on shared. That number scales linearly with how much CI you do.
A managed box is a flat monthly fee that doesn't move when you add a developer:
| Team | Pipelines/mo | Shared bill | Standard €29 | Pro €49 | 2× Pro €98 |
|---|---|---|---|---|---|
| Solo dev | 200 | €52 | saves €23 | saves €3 | costs €46 more |
| 5-person | 1,000 | €260 | saves €231 | saves €211 | saves €162 |
| 10-person | 2,000 | €520 | saves €491 | saves €471 | saves €422 |
| 20-person | 4,000 | €1,040 | saves €1,011 | saves €991 | saves €942 |
Crossover points (where flat beats per-minute):
- Standard (€29) pays for itself above ~110 pipelines/month — but only 4 slots, so slower wall-clock.
- Pro (€49) above ~190/month.
- 2× Pro (€98) — the configuration that also matches shared wall-clock — above ~375/month.
Below those, GitLab.com shared is genuinely cheaper (and, for one box, faster). Above them, you're paying GitLab a tax that grows with your team while a flat €49–98/mo just… doesn't.
What about wall-clock for bigger teams
The honest answer is the parallelism math above: buy slots to match your pipeline's width. A 13-job pipeline wants ~13+ slots to hit shared-like wall-clock — that's 2× Pro or one Heavy. Don't put a busy team on a single Starter and expect shared speed; that's the trade you'd be making for the rock-bottom price. Pick the tier (or box count) that covers your widest pipeline stage, and you get parity plus a bounded bill.
What we found testing dedicated (CCX) hardware
One surprise from this run: Hetzner's CX (shared) line hit capacity limits — at the moment we were provisioning, cx43/cx53 were temporarily unavailable across all three EU datacenters. So we also tested a dedicated CCX box (ccx33, 8 dedicated vCPU):
- It reached shared wall-clock parity even as a single box, and even cold (4:16) — no double-image-pull cliff, because there's only one box and the dedicated cores have no noisy neighbors.
- Dedicated capacity was available everywhere when shared wasn't.
Dedicated costs meaningfully more per core, and it's not a purchasable tier yet — this was a lab test. But the result is clear enough that a dedicated option for capacity-sensitive or wall-clock-sensitive workloads is on the roadmap. If that's you, get in touch and we'll prioritize it.
The self-hosted GitLab elephant in the room
If you run self-hosted GitLab (on-prem, behind a VPN, gitlab.your-company.dev), none of the shared-runner math applies to you at all. GitLab.com SaaS shared runners require a project on GitLab.com — your instance simply can't use them.
Your options:
- DIY runners. Spin up boxes, install gitlab-runner, manage updates, handle abuse, do backup/restore. It works — it's also a part-time sysadmin job.
- Managed CI that doesn't support self-hosted GitLab. Most managed-CI services target GitHub Actions — Blacksmith, for example, is GitHub-only and can't help a GitLab shop at all.
- Managed runners that DO support self-hosted GitLab. Runsetters is built for this — the runner dials outbound to your GitLab. Nothing on your side needs to be publicly reachable.
For the self-hosted segment the comparison isn't "shared vs managed cost" — it's "DIY sysadmin time vs a flat managed bill."
Run your own pipeline
The numbers above are ours. The ones that matter are yours: register a runner, point your .gitlab-ci.yml tags: at it, and capture the same wall-clock and per-job timings on your real CI.
Raw data for this run, including the dedicated box, is in docs/BENCHMARK.md. Better data welcome.
If you're running self-hosted GitLab and tired of either babysitting a runner box yourself or paying GitLab.com per minute for shared runners you can't even reach, check out Runsetters. Managed runners on Hetzner, flat monthly billing, works with any GitLab instance.
/ TRY RUNSETTERS
Stop buying CI minutes. Start renting machines.
Managed GitLab runners on Hetzner bare metal. Online in 2 minutes.
Use code SMOKETEST1
for a €1 trial on Starter.