Heroku to Railway: A Migration Runbook
Railway is the closest thing to Heroku’s developer experience, which makes this the most common move since Heroku entered sustaining engineering in February 2026.
It’s also close enough to be misleading. Four things break, and each has a specific fix.
What breaks
1. Build detection
Heroku used buildpacks. Railway uses Nixpacks, which does the same job — inspects your repo and infers how to build it — but not always with the same result.
Where it diverges: Nixpacks may pick a different language version than your Heroku buildpack did. If you relied on Heroku’s default rather than pinning, you can land on a different runtime.
The fix, and do it before migrating: pin your versions explicitly. .node-version or the engines field for Node, .python-version or runtime.txt for Python, .ruby-version for Ruby. Pinning is good practice anyway and it removes the whole class of problem.
If Nixpacks can’t work out your build, a Dockerfile overrides it entirely. That’s the escape hatch — and it’s also what you’d write if you later move to a plain VPS, so it isn’t wasted work.
2. The database, across Postgres versions
DATABASE_URL exists on both platforms and your app code generally won’t notice. The migration itself is where it goes wrong.
Check the Postgres major version on both sides first. pg_restore into an older major version fails; into a newer one it usually works. Heroku and Railway will not necessarily give you the same default.
# on Heroku
heroku pg:info --app your-app
# dump
heroku pg:backups:capture --app your-app
heroku pg:backups:download --app your-app
# restore into Railway (connection string from the Railway dashboard)
pg_restore --verbose --clean --no-acl --no-owner \
-d "$RAILWAY_DATABASE_URL" latest.dump
--no-acl --no-owner matters: Heroku’s dump carries role grants that don’t exist on the target, and without those flags the restore throws errors on every one.
Use pg_dump/pg_restore binaries matching the higher of the two versions. A mismatch here is the most common failure in this whole migration.
3. Scheduler and cron
Heroku Scheduler has no direct equivalent, and this is the one people discover in production three days later.
Railway supports cron schedules on a service, so the pattern is: define a service that runs your task command, give it a cron expression, and make sure it exits when finished rather than staying resident. A task that doesn’t exit is billed as an always-on service — see the billing section.
Before you cut over, enumerate every scheduled job. heroku addons won’t show them all; check Heroku Scheduler explicitly, plus anything triggered by an add-on.
4. Config vars pointing at add-ons
Copy your config vars across and about a third of them will reference things that no longer exist.
heroku config --app your-app --shell > heroku.env
Then read every line. The ones that break are the ones injected by add-ons — REDIS_URL, PAPERTRAIL_API_TOKEN, SENDGRID_*, anything named after a marketplace product. Each is a separate decision: provision the equivalent on Railway, keep using the vendor directly, or drop it.
Do not bulk-import the file. You’ll carry dead credentials into the new environment and spend an afternoon working out which of them matters.
The cutover, without downtime
- Lower your DNS TTL to 300 seconds, 24 hours ahead. Nothing else on this list works without it.
- Deploy to Railway and get it green with a test database. Build problems surface here, not at cutover.
- Enumerate and recreate scheduled jobs, disabled for now.
- Restore a recent snapshot into the Railway database and point the Railway app at it. Verify it boots and serves.
- Announce a short read-only window if your app can — ten minutes is usually enough and avoids all dual-write complexity.
- Final dump and restore during that window, so no writes are lost.
- Flip DNS. With a 300-second TTL, propagation is quick.
- Enable the scheduled jobs on Railway, and confirm they’re disabled on Heroku. Running both is how you send every email twice.
- Keep Heroku running 48 hours. Don’t delete the app — scale the dynos to zero so it stops billing but the data stays.
- Only then tear down.
If a read-only window genuinely isn’t acceptable, you’re into logical replication rather than dump-and-restore, and that’s a bigger project than this article. For most apps, ten minutes at 4am is the cheaper answer.
The bill has a different shape
This is the part that surprises people, and it isn’t about the amount — it’s about the model.
Heroku charged a fixed price per dyno. Railway bills per second for what your service consumes, plus a plan fee:
| Plan | Fee | Included credits |
|---|---|---|
| Hobby | $5/mo | $5 |
| Pro | $20/mo per workspace | $20 |
Usage rates: memory $0.00000386 per GB/second, CPU $0.00000772 per vCPU/second, egress $0.05/GB, volumes and object storage extra.
Those per-second figures are hard to reason about, so converted to monthly for an always-on service:
1 GB of memory held 24/7 costs about $10.01/month. One full vCPU costs about $20.01.
| Your service | Memory | CPU | Monthly usage |
|---|---|---|---|
| Light API, 512MB, ~5% CPU | $5.00 | $1.00 | $6.00 |
| Small web app, 1GB, ~10% CPU | $10.01 | $2.00 | $12.01 |
| Web + worker, 2GB, ~20% CPU | $20.01 | $4.00 | $24.01 |
| Busy app, 4GB, ~40% CPU | $40.02 | $8.00 | $48.02 |
Against Heroku’s Basic dyno at $7 and Standard-1X at $25, that’s broadly comparable at the small end.
The structural difference that matters: memory is billed while your service is resident, regardless of whether it’s doing anything. A web process waiting for requests still holds its memory. This is why Railway suits bursty workloads better than permanently-resident ones — we found the same thing pricing n8n, where a service that must stay awake to catch webhooks is billed as always-on because it is.
On spend caps
The brief for this article said to show you how to set a spend cap on day one. Railway’s pricing page doesn’t document a spending-limit feature, so we’re not going to tell you to enable one we can’t confirm exists.
What to do instead:
- Check your Railway dashboard for usage alerts or limits — the product may offer controls its pricing page doesn’t describe, and that’s worth two minutes on day one.
- Predict your bill from the table above before you migrate. Memory is the dominant term and you already know roughly how much your dynos use.
- Watch the first invoice closely, particularly egress at $0.05/GB, which is the line most likely to differ from your expectation.
- Make sure scheduled jobs exit. A cron task that hangs becomes a resident service billed by the second.
Is Railway the right destination?
Railway preserves the most of what people liked about Heroku — push to deploy, build inference, managed databases a click away, preview environments. If your objection to Heroku is its trajectory rather than its pricing model, it’s the smallest change you can make.
If your objection is the pricing model — you want a fixed number every month rather than a meter — then a VPS with Coolify replaces the whole platform for $12–24, and we compared all three routes with a real app’s costs on each.
How we checked this
Railway’s plan structure and usage rates — Hobby at $5/month with $5 included credits, Pro at $20 per workspace with $20 included, memory at $0.00000386 per GB/second, CPU at $0.00000772 per vCPU/second, egress at $0.05/GB and object storage at $0.015 per GB-month — are from Railway’s own pricing page, read in August 2026. The monthly conversions are our arithmetic at 30 days, and the CPU percentages in the table are illustrative profiles rather than measurements — your actual CPU utilisation is the variable we can’t know.
Heroku’s dyno pricing of $7 for Basic and $25 for Standard-1X is from its own pricing page.
On the spend cap specifically: Railway’s pricing page does not describe a spending-limit or usage-cap feature. We looked, we didn’t find one documented, and we’ve said so rather than writing instructions for a feature we couldn’t verify. If it exists in the dashboard, that’s better than our reading — check.
What we did not do, and it matters for a runbook: we have not performed this migration. The commands above are standard pg_dump/pg_restore and Heroku CLI usage rather than steps we executed against a real app, and the cutover sequence is ordered so that nothing is destroyed until the replacement is verified — which is the property that matters most if any step differs for your stack.
The four failure modes are the ones this migration is documented to produce — build detection, Postgres version mismatch, scheduler gaps and add-on config vars — rather than a ranked list from our own incident log.
The Railway link above is an affiliate link. Heroku, Nixpacks and Coolify are named and unlinked.
FAQ
How do I migrate a Heroku app to Railway?
Pin your language versions, deploy to Railway with a test database, restore a Postgres dump with --no-acl --no-owner, recreate scheduled jobs, lower DNS TTL 24 hours ahead, take a short read-only window for the final dump, then flip DNS and keep Heroku warm for 48 hours.
Will my buildpack work on Railway?
Railway uses Nixpacks rather than buildpacks. It usually infers correctly, but may choose a different language version — pin yours explicitly before migrating, or add a Dockerfile to override detection entirely.
How do I move my Heroku Postgres database?
heroku pg:backups:capture then download, and pg_restore --clean --no-acl --no-owner into the Railway connection string. Check the Postgres major version on both sides first — restoring into an older major version fails.
What replaces Heroku Scheduler on Railway?
A service with a cron schedule that runs your task command. Make sure it exits when done — a task that stays resident is billed as an always-on service.
Is Railway cheaper than Heroku?
Broadly comparable at the small end: a 1GB service with light CPU is about $12/month of usage against a $7 Basic dyno or $25 Standard-1X. The difference is the model — Railway meters, Heroku didn’t.
Can I set a spending limit on Railway?
Railway’s pricing page doesn’t document one. Predict your cost from the memory and CPU rates instead, check the dashboard for usage alerts, and watch your first invoice — especially egress at $0.05/GB.
How do I avoid downtime?
Lower DNS TTL to 300 seconds a day ahead, use a short read-only window for the final database sync, and keep Heroku running with dynos scaled to zero for 48 hours after the flip.