Back to Blog

SQLite Is Probably Good Enough for Your Side Project

May 08, 2026
5 min read
SQLite Databases Backend Architecture Web Development

Every side project I’ve seen die in the setup phase had one thing in common: the database config came before the idea had a working prototype. Postgres spun up in Docker, a connection pooler configured, a managed cloud instance provisioned. The project never shipped. The infrastructure is still running.

SQLite removes that entire category of problem. And in 2026, it’s capable enough that “good enough for a side project” undersells it.


1. What SQLite Actually Is

Most people know SQLite as an embedded database. The part that gets underemphasized is what “embedded” means in practice: the database is a single file, the engine runs in the same process as your app, and there’s no server to connect to. No TCP socket. No auth config. No connection pool. You open a file and run queries.

That simplicity is not a limitation. It’s the whole point.

import sqlite3

conn = sqlite3.connect("app.db")
conn.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
conn.execute("INSERT INTO users (name) VALUES (?)", ("Nits",))
conn.commit()

That’s the full setup. No Docker. No environment variables for a connection string. No waiting for a server to be healthy. The file is the database.

SQLite is also not a toy. It’s the most widely deployed database engine in the world by a large margin - every Android device, every iOS app, every browser, Firefox’s storage layer, parts of macOS itself. The engine is extraordinarily well-tested. The file format is stable and explicitly designed for long-term archival.


2. The Write Limitation Is Real, But Probably Not Your Problem

The honest limitation: SQLite uses file-level locking for writes. One writer at a time. Concurrent reads are fine. Concurrent writes queue up.

For most side projects, this is not a bottleneck you will ever hit. A project doing hundreds of writes per second is not a side project anymore. If you’re at that scale, you have other problems that matter more, and you’ll have time to migrate before the write lock becomes the thing that breaks you.

WAL mode makes this less of an issue than it used to be.

conn = sqlite3.connect("app.db")
conn.execute("PRAGMA journal_mode=WAL")

With WAL enabled, readers don’t block writers and writers don’t block readers. Concurrent reads and a single writer proceed simultaneously. For a typical web application with mostly reads and occasional writes, WAL mode performance is competitive with a networked database running locally. The network round-trip you save often outweighs the write serialization.


3. Deployment Got Simpler

The operational story for SQLite used to be: fine locally, awkward in production. Serverless platforms didn’t have persistent filesystems. Containers got recreated and took the database file with them.

That changed. Fly.io has persistent volumes. Railway gives you persistent storage. If you’re running a VPS, your database is a file on disk like anything else. Litestream replicates a SQLite database to S3 continuously, so backups are not a manual process you forget to set up.

# litestream.yml - continuous replication to S3
dbs:
  - path: /data/app.db
    replicas:
      - url: s3://your-bucket/app.db

Litestream streams WAL frames to S3 in near real-time. Recovery means restoring from S3 and replaying. The operational overhead is low and the setup is one config file.

Turso took this further by building a distributed SQLite service - SQLite semantics with edge replication. If you want SQLite locally, in production, and replicated to multiple regions, that’s now a thing you can actually do without managing it yourself.


4. The ORM Support Is There

If you’re worried that choosing SQLite means giving up a good ORM experience, it doesn’t.

Prisma supports SQLite. Drizzle supports it. Django’s ORM has supported it since 1.0 - in fact, SQLite is the default when you start a new Django project. The schema migration story is the same. The query interface is the same.

# Django settings - no Postgres config, no credentials, just this
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

The only things that don’t translate directly are Postgres-specific features: JSONB columns, full-text search via pg_trgm, PostGIS. SQLite has JSON support and FTS5 for full-text search, but if your project depends on advanced Postgres features, you probably already knew you needed Postgres.


5. When to Reach for Postgres Anyway

SQLite is the right call until it isn’t. A few actual reasons to start with Postgres:

Your team is working on it together and multiple developers need to connect to the same database simultaneously from different machines. SQLite is a file - sharing a file across machines for dev is annoying. Postgres in Docker is the right call here.

You know your write volume will be high at launch. Not “might be high” - actually know. If you’re building something where thousands of users will be writing concurrently from day one, start with Postgres. But most things don’t start there.

You need Postgres-specific features. Arrays, JSONB indexing, logical replication, pg_vector for embeddings. If those features are load-bearing for your idea, don’t start with SQLite and plan to migrate.

Everything else: SQLite is fine. Ship the thing. Migrate if you need to. The migration from SQLite to Postgres is not painless, but it’s a solved problem you’ll deal with when you have users, which is a better problem to have than the infrastructure being done and the project not existing.