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

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