Your team likely uses the wrong database
Postgres is the default. SQLite plus a KV store might be the better call for most production apps today and the reasons are simple
Building a game taught me something no tutorial bothered to mention: most of your data doesn’t need to know the rest of your data exists.
The game had chat, auth, player state, match history, and inventory. The default instinct is to pull everything into one database and start drawing foreign keys. That’s what the tutorials show and it feels like the only way.
So I put each domain into its own SQLite database and realized most of these systems never needed to talk to each other at all.
Chat doesn’t need to know anything about inventory. Auth doesn’t need to join against match history. These are separate domains that only feel related because they live inside the same application, and the monolithic database (Postgres) was flattening that distinction, encouraging joins that should never have existed in the first place. The deeper insight came from chat specifically. Chat data is voluminous but has a natural lifespan. SQLite sharding made it easier to manage chat by year. In 2026, the 2026 database is used. In 2027 a new one gets scaffolded while the old one is retired. That is not a workaround. It is the correct mental model for data with a natural lifespan, one that I never thought about because Postgres leads you to think of your database as permanent infrastructure rather than a purposeful container that gets provisioned when needed and retired when the data’s useful life ends.
Chances are your team is making the same assumptions that felt safe then but aren’t serving you now.
The Hidden Cost of Playing It Safe
Your business is probably running on Postgres right now. Not because anyone evaluated the options or the workload demanded it, but because the choice was automatic and it felt like the responsible choice when the project kicked off.
It probably wasn’t.
The moment your team spins up a managed Postgres instance on RDS or Supabase, you start paying. Not just money, though that adds up fast. You pay in operational complexity that quietly becomes your team’s problem the moment something goes wrong at the worst possible time.
Connection pooling is the first thing you meet. Postgres wasn’t designed to handle hundreds of simultaneous open connections, so you need a separate tool just to manage that. Then comes autovacuum: a background process that reclaims storage space on its own schedule and occasionally decides to do it during a traffic spike your team didn’t see coming. Then the query planner, which is Postgres’s internal engine for deciding how to execute your SQL, making reasonable decisions most of the time and baffling ones the rest. None of this is exotic. It’s just the standard overhead that comes bundled with the tool, whether your team wanted it or not, and it lands on whoever drew the short straw for on-call that week.
Now ask honestly: does your app actually need all of this? If your app is a SaaS product, a content management system, or an internal tool, the answer is likely no.
Most apps are read-heavy and write-light, running in a single region with one server, a few hundred concurrent users at peak, and a schema that hasn’t changed fundamentally in years. For that app, Postgres is a sledgehammer aimed at a finishing nail.
SQLite Is Not What Your Team Thinks It Is
Most teams hear SQLite and picture a toy, something for mobile apps, local prototypes, or a junior engineer’s side project. That mental model is years out of date.
SQLite supports window functions, full-text search, JSON operators, and WAL mode, which stands for write-ahead logging and enables concurrent reads without blocking writes. It is the most widely deployed database engine in the world, embedded in browsers, operating systems, phones, and devices your team uses every day. It is a battle-tested database.
More importantly, SQLite is honest. It does exactly what you tell it to. There is no background process silently reorganizing your data, no query planner making choices nobody on your team asked for, and no connection overhead adding latency before a query even starts. A local SQLite query can respond in under a millisecond because there is no network hop, no round trip, and no negotiation standing between your code and your data. What your team writes is what runs.
The One Real Limitation, and How to Design Around It
SQLite has a single writer lock, meaning only one write can happen at a time. For genuinely write-heavy workloads, that ceiling is real and your team will eventually hit it.
But a known constraint is a gift.
Pair SQLite with a Key-Value store like Redis [1].
The KV store becomes your write buffer, where high-frequency or non-urgent writes land first before a background process batches them and flushes to SQLite in bulk transactions, which are significantly more efficient than individual writes anyway. For writes that need an immediate read-back, say a user updating their profile and expecting to see the result right away, the KV holds that data in memory while the flush completes, and SQLite handles the relational query once the write is durable.
Auth sessions and tokens are the bigger wins. They have no business touching your database on every request, yet in most teams they hammer the primary database on every page load, every API call, and every middleware check. Move them entirely into the KV layer and your team eliminates that entire class of high-frequency, low-value reads in a single architectural decision.
The result is a setup where your KV handles sessions, caching, and write buffering, while your SQLite handles structured, relational, durable data. Each layer does one job. The complexity required is visible and deliberate.
The Industry Is Already Figuring This Out
Cloudflare built D1 on distributed SQLite. Turso distributes SQLite across regions with embedded replicas. Fly.io built LiteFS to replicate SQLite volumes across nodes. These are serious infrastructure companies placing serious engineering bets on the same premise.
Design Like It Matters
When you give each concern its own SQLite database, your team starts asking questions that a monolithic Postgres wouldn’t permit. Which data needs to be relational? Which data is ephemeral? Which can be eventually consistent? Which can live in parallel to everything else?
You stop throwing everything into one place simply because you can. You start designing deliberately, with each layer doing a job that your team consciously assigned to it. That deliberateness changes how your team reasons about the system, how you onboard new engineers, and how you make tradeoffs under pressure. Constraint, more often than not, is the mother of good architecture, and SQLite forces your team to confront the shape of your data in a way that Postgres doesn’t.
So What Should Your Team Do?
For typical SaaS applications, content platform, or CRUD-driven system with <5k writes/sec running in one or a few regions, this pattern would improve speed while reducing cost and operational complexity. Your team doesn’t need a managed database costing hundreds of dollars a month.
Your team needs to use the right tool for the workload you actually have, not the one the ecosystem says is the default.
For most teams, that tool is SQLite.
Blobs go on object storage. Obviously.
Footnotes
[1] Redis is strongly consistent. It has persistence, replication, and durability guarantees. But if you’re still skeptical, add a cron job that syncs from Redis to SQLite every 5 seconds. Worst case: you lose 5 seconds of high-frequency writes. For the vast majority of apps (under 5k writes/sec), that’s an acceptable trade-off for eliminating the writer lock bottleneck entirely. There are other options like Cloudflare KV or Upstash but Redis is a good default.