Skip to main content

One Team Picked Railway + SQLite Over Heroku Postgres: The $5 vs $12 Database Math Small Apps Get Wrong

12 min readDora NodaDora Noda
Share
On this page

A developer's portfolio site had an embarrassing failure mode: every deploy wiped the database. The app ran SQLite inside a Heroku Docker container, and Heroku's ephemeral filesystem threw the whole database file away on each restart — so real blog and case-study content entered through the frontend simply vanished. The fix was written up as a proper architecture decision record, ADR-028, and the decision was blunt: migrate to Railway with a persistent volume for about $5 a month and zero code changes, instead of doing the "responsible" thing and migrating to Heroku Postgres for $10–12 a month.

That two-to-one gap is the lead, not the footnote, because the ADR priced five options side by side and the responsible-looking answer lost on arithmetic. This post recomputes every line of that math against September 2026 list prices, shows where a $0.03 S3 bucket closes the durability gap the $12 option charges $7 extra for, and names the exact workload shape where SQLite stops being honest — because the cheapest correct answer for a single-node app turns out to be a SQLite file on a volume you already pay for, with no database line item at all.

The ADR in 30 seconds, then the table​

The source is a real decision record: ADR-028 in reillysteere/developerprofile, status Accepted, February 7, 2026. The app is a single-author portfolio site on SQLite plus TypeORM, projected under 100 MB, low write concurrency, deployed as one instance. Requirements: data survives deploys, migrations apply without data loss, backups exist, GitHub integration and deploy logs stay, minimal code changes, roughly $5–10 a month.

Reference workload for every number below — the ADR's own shape, kept deliberately boring: one single-node app, SQLite file under 100 MB growing toward 1 GB, low write concurrency, one always-on instance, prices observed September 2026, all normalized to dollars per month. Re-check live pricing pages before budgeting; the deltas matter more than the cents.

OptionComputeStorageExtrasTotalDelta vs Postgres baseline
Heroku dyno + Postgres (baseline, "the $12")Basic dyno $7 (Eco $5)Mini/Essential-0 Postgres $5—$10–12$0
Heroku + Litestream + S3Dyno $5–7$0 (ephemeral + stream)S3 ~$0.03–0.25~$5.05–7.25−$5ish
Railway Hobby + volume ("the $5")Hobby $5 (incl. $5 usage credit)1 GB × $0.15—~$5.15−$5 to −$7
Render Starter + diskStarter $71 GB × $0.25—~$7.25−$3 to −$5
Fly.io shared VM + volume~$1.94–3.571 GB × $0.15—~$2–5−$5 to −$10
Owned box (Hetzner-class) + SQLite file$0 incremental (box already paid)$0 incremental$0$0−$10 to −$12

Title shorthand, disambiguated as the reviewer demanded: "the $5" is the Railway Hobby-plus-volume row (~$5.15 at 1 GB), and "the $12" is the top of the Heroku dyno-plus-Postgres range (a $7 Basic dyno plus $5 Essential-0 Postgres). The bottom of that range, $10, is an Eco dyno plus Mini Postgres. Everything that follows defends each cell.

The full math, line by line​

Heroku dyno + Postgres: $10–12. Heroku's cheapest data plans are the Mini Postgres at $5 a month and the Essential-0 at about $0.007 an hour capped at $5 a month — confirmed in Heroku's own help docs and Dev Center output as recently as May 2026. The compute half is an Eco dyno ($5, with 1,000 shared dyno hours) or a Basic dyno ($7). Add them: $5 + $5 = $10 at the floor, $7 + $5 = $12 at the honest always-on ceiling. That matches the ADR's $10–12 exactly, and it also prices the migration nobody mentions: porting SQLite to Postgres costs 8–12 hours of rewrite work on top, per community migration notes — a bill measured in evenings, not dollars.

Heroku + Litestream + S3: ~$5.05–7.25. Keep the same $5–7 dyno, skip Postgres entirely, and stream the SQLite write-ahead log to object storage with Litestream. The S3 half is the famous part: the canonical self-hosted example reports about $0.03 a month in bucket costs for a small database, and even padded tenfold for a 1 GB file with versioned history it stays under a quarter. Total: the dyno you already pay for, plus pennies. The catch is complexity, not money — a sidecar process, WAL-mode pragmas, and restore-on-boot wiring — which is exactly why the ADR priced it and passed.

Railway Hobby + volume: ~$5.15. Hobby is $5 a month including $5 of usage credit, and volumes bill at $0.15 per GB-month (about $0.00000006 per GB-second on the meter). One GB of SQLite therefore costs fifteen cents. The ADR's "~$5/mo" holds up to within a dime at this scale, with one honest asterisk the ADR itself records: cron jobs that fire every 10 minutes can keep the service from ever sleeping, so budget the always-on end of usage-based pricing rather than the serverless dream.

Render Starter + disk: ~$7.25. Starter is a flat $7 for 0.5 CPU and 512 MB that never sleeps, and persistent disks are $0.25 per GB-month on paid tiers only — the free tier has no persistent disk, so SQLite resets on every deploy there. One GB lands at $7.25, matching the ADR's "$7+/mo" to the quarter. Note the structural difference from Railway: Render's number is flat, so it neither rewards idleness nor punishes constancy.

Fly.io shared VM + volume: ~$2–5. The cheapest always-on shape is a shared-CPU VM around $1.94–3.57 a month plus a volume at the same $0.15 per GB-month Railway charges. At 1 GB that is roughly $2–4 — the cheapest hosted row in the table, which is why the ADR lists "$0–5/mo" — but the ADR also dings it for a less integrated GitHub experience: CLI-driven deploys instead of Railway-style autodeploy. Cheap is not free; the discount is paid in workflow friction.

Sensitivity: what moves each number. Recompute at 10 GB instead of 1 GB and the storage legs separate: Railway adds $1.50, Render adds $2.50, Fly.io adds $1.50, S3 adds roughly a dollar — while the Heroku Postgres row does not move at all until the Mini/Essential row cap forces a plan jump, at which point it jumps by tens, not cents. Bandwidth is the other lever: Render includes 100 GB egress before $0.10 per GB overage, Railway meters egress per GB, and the owned-box row includes terabytes (a Hetzner-class CX22 at about €4.35 a month ships with 20 TB). The pattern: metered options win small and punish growth in different places, while the flat and owned rows barely notice a 10x storage increase.

What Litestream plus S3 actually buys​

The objection that kills SQLite in every design review is the single-file failure mode: the host dies, the file dies with it. Litestream, the open-source WAL tailer created by Ben Johnson and now maintained under Fly.io, answers it mechanically. It watches the SQLite write-ahead log and streams every committed transaction to S3-compatible storage continuously — one production integration measured recovery-point objective improving from 6 hours on cron snapshots to about 1 second on continuous streaming — with compression and retention handled automatically and restore as a single command on boot.

The configuration is small enough to quote in full: enable WAL mode, set synchronous=NORMAL with a busy timeout, point a litestream.yml at a bucket, and run the app under litestream replicate -exec. The documented gotchas are real but bounded — WAL mode is mandatory, a Litestream version bump once caused socket accumulation against R2 that needed a pin, and replication liveness deserves its own health check rather than trusting the app's /health — but the durability story is production-grade for pennies, and multiple 2026 production setups pair exactly this with point-in-time restore.

So why did the ADR pass on the cheapest correct row? Because "pennies plus a sidecar" still means owning the sidecar: writing the entrypoint wrapper, wiring restore-if-missing on boot, and debugging replication stalls at 2 a.m. The $5.10 difference between this row and the Railway row is the price of never thinking about the sidecar. For a portfolio site, the ADR judged that worth paying. For a team with three SQLite services, the sidecar amortizes and this row wins — which is precisely the sensitivity analysis the title promised.

The fine print inside each volume​

Every "$0.15/GB" in the table hides a constraint, and the ADR is admirably honest about the one that mattered most: on Railway, volumes cannot be used with replicas — single instance only, with a few seconds of deployment downtime while the volume remounts. For a single-author site that is acceptable. For anything needing horizontal scale or zero-downtime deploys, it is disqualifying, and no per-GB price fixes it.

The rest of the fine print, verified against 2026 docs and operator notes: Render disks exist only on paid tiers, come with daily snapshots, and attach to one service — the free tier's ephemerality is the trap the ADR's problem statement already fell into once. Fly.io volumes are NVMe local disks at $0.15 per GB-month that attach to exactly one machine, which is why Fly pairs them with LiteFS for multi-region replication — a whole second system to learn the day one box stops being enough. Railway volumes share the single-attach shape, and backups restore only to the same environment, so a staging-prod clone story needs separate volumes and a copy step. None of these invalidate the table; they define the workload envelope the table is valid inside. That envelope — one instance, one region, backups for disaster recovery rather than cloning — is exactly the ADR's shape, and the post would be lying if it recommended these rows outside it.

Parity check the other direction: what does the $12 row include that the $5 rows do not? A managed Postgres brings automated backups with point-in-time recovery, connection pooling, a separate failure domain from the app dyno, and an upgrade path to multi-instance high availability without re-architecting storage. The Mini tier's limits are real (10,000 rows on Mini per Heroku's FAQ — this is a starter tier, not a forever home), but the operational surface is someone else's pager. The $5 rows trade all of that for a file and a backup schedule. For under 100 MB and one writer, that trade is correct. It stops being correct at the boundary the next section draws.

When SQLite stops being honest​

SQLite's limit is not size, it is concurrency shape. In WAL mode on NVMe storage, SQLite sustains 10,000–50,000 writes per second on a single node — comparable to a tuned Postgres for single-writer workloads, because the engine configured for 2004 hardware is running on 2026 SSDs. The wall is structural, not throughput: one writer at a time, no multi-instance high availability, no read replicas off the same file without a replication layer, and connection counts that assume an in-process library rather than a networked server pool.

Concretely, migrate toward Postgres when two or more of these turn true: multiple app instances need concurrent writes; write concurrency exceeds what a single WAL writer clears; the working set or availability target needs replicas with failover rather than restore-from-backup; or tenant isolation demands per-connection guarantees Postgres enforces natively. The ADR names its own exit ramp — TypeORM makes the future migration straightforward — and that is the right way to hold it: SQLite by default for the single-node shape, Postgres when the concurrency shape says so, never as a reflex.

And the $0 row: a SQLite file on an owned volume has no database line item because the box is already paid for — a Hetzner-class CX22 around €4.35 a month with terabytes of included bandwidth absorbs a 100 MB database file without noticing. The platform posture matches: Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own — and it deliberately never runs your database for you; your SQLite file rides on your volume, your backups land in your bucket.

Which one should you pick​

Covering all six rows, the decision rule: stay on Heroku plus Postgres if the app already speaks Postgres and $10–12 fits — the migration nobody prices is the one you skip. Pick Heroku plus Litestream if you must stay on Heroku's substrate but can own a sidecar to save ~$5 a month. Pick Railway Hobby plus volume if zero code changes and GitHub autodeploy beat absolute cheapest — the ADR's choice, and the right one for its shape. Pick Render Starter plus disk if flat pricing and never-sleeps simplicity beat shaving $2. Pick Fly.io if CLI-driven deploys are fine and the $2–5 floor matters most. Self-host the file on an owned box the day the whole app fits on hardware you already rent — the database line goes to zero and stays there until the concurrency section above says otherwise.

The through-line of ADR-028 is not "SQLite beats Postgres." It is that the default choice — reach for managed Postgres because production means Postgres — cost this team $7 extra a month plus a rewrite, to solve a persistence problem a fifteen-cent volume already solved. Defaults are expensive when they answer a question nobody asked. Price the question first; the database picks itself.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Check your move before you migrate

Free browser tools: check a render.yaml or your Render scripts against bex, or turn a Heroku app or docker-compose.yml into a draft render.yaml. Nothing you paste leaves your browser.

Open the migration tools