Skip to main content

50 Pull Requests, 50 Live Environments: What Preview-at-Scale Really Costs on Railway vs Owned Hardware

12 min readDora NodaDora Noda
Share
On this page

Fifty open pull requests means fifty live environments — and at that scale, the preview-environment question stops being about developer experience and starts being about money. Run them as auto-managed PR deploys on Railway's per-minute meter and the bill lands near $585 a month. Run the same fifty as ephemeral namespaces on a Cluster-API fleet's Hetzner-backed nodes and the marginal hardware costs roughly $95 a month always-on, closer to $30 once idle previews actually sleep. Same pull requests, same containers, a 6x to 18x gap — and the entire spread comes down to three things: what an idle preview costs, who pays for the orchestration, and whether your health checks let anything stay idle at all.

50 concurrent PR previews, steady stateRailway (Pro meter)Hetzner fleet (marginal)
Compute for 50 envs~$562/mo€84 ($95)/mo always-on
Idle-culled (previews sleep when untouched)~$190/mo, if they sleep€28 ($30)/mo
Egress (reviewer traffic)~$5/mo~$0 (GBs vs 20 TB included)
Orchestration (Namespace lifecycle, routes, certs)included, hiddencontroller you run once
Approximate monthly total~$585 (~$210 sleeping)~$95 (~$30 sleeping)

The workload behind every number below is fixed and stated up front so you can plug in your own: each preview runs two services — an API at 0.5 vCPU / 512 MB and a web frontend at 0.25 vCPU / 256 MB, or 0.75 vCPU and 0.75 GB per PR. Previews share one staging Postgres rather than each provisioning a database (per-PR databases would change both bills beyond recognition). Fifty of them are live at once, all month. Railway rates are the July 2026 Pro card both this list's reaudit and third-party exporters verify: $0.000231 per vCPU-minute, $0.000116 per GB-minute, $0.05 per GB egress, metered by the minute. Fleet units are CX22-equivalents at €4.49 (2 vCPU, 4 GB, 20 TB included traffic).

Nothing below hides a variable — where an assumption moves the answer, it gets its own row.

What 50 previews actually are as Kubernetes objects

Strip the marketing and one preview environment is five objects: a Namespace, a Deployment or two, a Service, an HTTPRoute rule, and a TLS identity — created when the PR opens, deleted when it merges. Multiply by fifty and the fleet holds roughly 50 Namespaces, 100 Deployments, 100 Services, and 50–100 HTTPRoutes churning at PR frequency: opens, force-pushes, and closes, several times a day each. That churn rate is the shape most preview-environment math quietly ignores, and it is where the interesting costs live — not in the steady-state CPU, which is trivially countable, but in everything that moves every time a route appears or disappears.

Per-namespace accounting is the easy part. Fifty previews at 0.75 vCPU and 0.75 GB each is 37.5 vCPU and 37.5 GB of requests — CPU-bound against a 2-vCPU/4-GB node shape, or about nineteen CX22-equivalents. Enforce it with a ResourceQuota plus a LimitRange in each preview namespace so one PR's load test cannot eat the other forty-nine, and put a default-deny NetworkPolicy on every preview namespace with explicit allows to the shared staging database.

That trio — quota, limit range, network policy — is the entire isolation story, and it is worth naming because it is also the whole answer to "what does the fleet version of Railway's environment boundary look like": a namespace with a quota, not a second cluster, not a vCluster per PR. A dedicated control plane per preview would multiply etcd and API-server load by fifty for zero additional isolation; the namespace is already the boundary Kubernetes designed for this.

The Railway side: what per-minute metering does to 50 always-on envs

Railway's own CI/CD writeup is admirably honest about the model: PR environments bill per minute for CPU, memory, egress, and storage, run for the hours the review takes, and cost nothing once deleted. There is no fixed penalty for creating environments frequently — which is exactly why the bill tracks concurrent PRs times lifetime so linearly. At 43,200 minutes in a month, one preview at 0.75 vCPU and 0.75 GB costs $7.48 in CPU plus $3.76 in memory, or $11.24. Times fifty: $562 a month, plus the $20 Pro seat base and roughly $5 of reviewer egress. Hobby-plan rates are double, which would put the same fifty near $1,130.

A team with fifty concurrent PRs is not on Hobby, but the 2x Hobby/Pro split is worth knowing the moment anyone extrapolates this math downward to a side project.

Two footnotes that both favor Railway before the verdict turns. First, each preview duplicating its services is the point — no controller to write, no route reconciler to babysit, no 3 a.m. page because a finalizer stuck on a deleted namespace. Our earlier build-vs-buy put that automation at roughly three engineering-weeks to build; Railway amortizes it across every tenant. Second, Railway's serverless sleep genuinely zeroes compute for idle services — a preview asleep sixteen hours a day costs a third, about $190 a month for all fifty. Whether your previews achieve that sleep is a probe-design question, and it is the same question that decides the fleet side. Which brings us to the part of the bill neither rate card mentions.

The fleet side: nodes, shared layers, and route churn

Nineteen CX22-equivalents at €4.49 is about €84 a month — call it $95 — for the naive always-on case. But nobody runs fifty previews always-on, because most of them are idle most of the time: awaiting review, awaiting CI, awaiting a human who is asleep. Cull the idle with scale-to-zero (KEDA HTTP or an equivalent request holding proxy, idleAfter in the low minutes) and the warm resident set at any moment is closer to a third — six or seven node-equivalents, roughly €28 a month. The honest fleet number is therefore a range, not a point: $30–95 a month in marginal hardware, on nodes the fleet already runs, with the low end requiring the probe discipline described below.

Image pulls are the second fleet cost people overestimate and the first one that bites at PR frequency. Fifty previews do not mean fifty full image pulls per push: every PR build shares the same base layers (runtime, OS, dependencies — typically hundreds of megabytes that never change between commits) and differs only in its thin app layer. Pull each unique layer once per node and the marginal pull per new PR commit is tens of megabytes, served peer-to-peer with something like Spegel, which turns every node's containerd image cache into a cluster-wide mirror with no persistent storage of its own.

Two caveats keep this honest. Kubelet image garbage collection evicts cold images once disk pressure crosses its thresholds, so a preview that slept for two days re-pulls on wake — cold-start latency stays dominated by image size, exactly as the scale-to-zero coverage on this list has shown. And Spegel's own issue tracker documents stale peer entries causing 1–3 minute pull delays after node scale-downs — the cache is a warm-pool optimization, not a correctness layer, so keep the upstream registry fallback working and test it.

Route churn is the genuinely load-bearing fifty-PR problem. Fifty previews mean on the order of a hundred HTTPRoutes appearing, updating, and disappearing daily against one shared Gateway — and the Gateway API ecosystem has receipts for what that does. Envoy Gateway issue #9536 documents route status updates silently stalling near ~306 HTTPRoutes across ~30 namespaces under ArgoCD-driven watch-event churn, even while data-plane routing stays correct: the routes work, but the control plane stops telling you so. Independent gateway-api-bench runs find Envoy Gateway write-heavy and slow on setup and teardown relative to peers. At fifty PRs you are below the ~300-route cliff but well into the zone where every force-push reconciles.

So halve the object count by giving each preview one HTTPRoute with two rules (API + web) instead of two routes, keep one shared Gateway rather than one per preview, and terminate TLS on a single wildcard certificate. That last one is not optional hygiene: Let's Encrypt allows fifty certificates per registered domain per week, and fifty churning previews will find that ceiling fast. A wildcard cert via cert-manager DNS-01, minted once, ends the entire category of problem.

Whether idle previews stay idle is a probe-design question

Here is the number that quietly decides both columns of the opening table: what fraction of your fifty previews is warm right now? Every "sleeping" estimate above — Railway's $190, the fleet's $30 — assumes an untouched preview actually sleeps. And as this list's health-checks coverage showed, an HTTP GET for /healthz from a load balancer and an HTTP GET for / from a paying reviewer look identical to anything counting traffic — so every component that is supposed to talk HTTP becomes a false wake source the moment a workload can go to zero.

The false-wake lineup for previews is specific and checkable: the platform's own health checker polling each preview's public URL, your uptime monitor (Datadog, Better Uptime, the status page) hitting all fifty hostnames every minute, the search-engine crawler that found the wildcard DNS, and a teammate's pinned tab auto-refreshing. Each one resets the idle timer; together they convert "sleeps sixteen hours a day" into "never sleeps," and the sleeping estimates in both columns silently become the always-on ones.

Note the subtlety the earlier post nailed down: at zero replicas there is no kubelet probe running — the wake comes from the layer above the pods, the gateway or monitor that still believes the service should be reachable. Tuning livenessProbe.periodSeconds on the Deployment fixes nothing; the fix lives one hop higher, at the proxy that classifies host plus path.

That fix — answer probe and monitor paths inline at the gateway, wake only on real user traffic, keep one deep check every 15–30 minutes that is allowed to wake — is also where the two sides diverge structurally. On your own fleet you own the gateway, so you can split the lanes: /healthz returns 200 from the proxy without waking anything, reviewer traffic queues and wakes. On Railway you cannot split lanes; routing policy is the product, and a monitor pointed at fifty preview URLs is fifty workloads that never sleep. The practical consequence cuts against the naive reading of the meter: Railway's per-minute billing rewards sleeping, but its managed routing gives you no lever to protect sleep from your own observability. Budget the Railway column at always-on unless you have proven — from the usage graph, not the config — that your previews actually go quiet overnight.

Isolation, sensitivity, and the verdict

Isolation first, because cost without it is a trapdoor. Railway's preview boundary is an environment: separate services, separate variables, private networking between them, public URLs per service. The fleet's boundary is a namespace with a quota, a limit range, and a default-deny network policy — equally tenant-grade for PR review traffic, with one asymmetry worth stating plainly. Railway's control plane (builds, deploys, secret injection) is someone else's audited surface; the fleet's preview controller is code you own, and its failure modes (stuck finalizers, leaked namespaces, a wildcard cert that lapsed) are yours to monitor. Put a reaper on preview namespaces with a TTL label and alert on namespace count, and most of that risk becomes a dashboard.

Then the sensitivity table, because "fifty" was never the point — the shape of the answer is:

Concurrent previewsRailway (~$11.24/env)Fleet marginal (culled)
10~$112/mo~$6/mo
50~$562/mo~$30/mo
100~$1,124/mo~$60/mo

Both sides scale linearly in concurrent PRs — the fleet's slope is just ~19x flatter, because its unit is a slice of a €4.49 node and Railway's is per-minute metered RAM. Lifetime multiplies the Railway column directly (a preview that lives four days costs twice one that lives two) while barely moving the fleet column (steady-state concurrency is what sizes nodes). So the verdict splits by team shape, not by ideology: below ~15 concurrent previews, Railway's zero-build, zero-maintenance PR deploys are worth more than the few hundred a month the fleet would save — our build-vs-buy put the crossover near 20 concurrent PRs for bought-vendor tooling, and Railway's deeper integration pushes it a little further. Past that, at fifty and beyond, the meter bills over $7,000 a year — five figures once you pass a hundred concurrent previews — for orchestration your fleet's controller does for the price of one engineer's careful week plus pocket-change hardware. The teams that should feel this most are the ones whose PR count already grew past the decision they made when previews were a checkbox: re-run your own two numbers — concurrent previews times lifetime — once a quarter, because the answer moves with headcount whether you revisit it or not.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Namespace-per-PR previews with idle culling and wildcard TLS are exactly the kind of controller a self-hosted PaaS should ship once. Star the repo on GitHub or deploy your first app today.

Sources

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex