RUNSETTERS
← Back to blog
· 8 min read #benchmark #gitlab #ci #hetzner

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 master commit we benchmarked was red — a Doctrine schema/mapping desync made doctrine:schema, composer:audit, and the four phpunit:functional jobs 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:

  1. 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.
  2. 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.
  3. 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.