← Back to Cooldecode
Week 3 · Lesson 13

SQLite: when it fits, and when it really doesn't

A full relational database that ships as a single file with no server process. That single design choice explains almost everything about where it shines and where it breaks.

1. What SQLite actually is

Every database you've likely heard of before this — Postgres, MySQL, SQL Server — is a client-server system: a separate process (the database server) runs somewhere, listens on a network port, and your app connects to it as a client, even if that server is running on the same machine.

SQLite throws that model away. There is no server process. The "database" is just a single file on disk (commonly ending in .db or .sqlite), and SQLite is a C library that gets linked directly into whatever program wants to read or write that file. When your app calls a SQL query, it isn't talking to another process over a socket — it's calling a function in its own process that reads and writes the file directly.

It still speaks real SQL (most of the standard: SELECT, JOIN, transactions, indexes, foreign keys) and is fully ACID-compliant. It's not a toy or a "lite" subset of relational features — the name refers to the lightweight, embedded nature of the engine, not a lightweight feature set.

2. Why it exists — and why "serverless database" is the right way to think about it

SQLite was built for the case where running and maintaining a separate database server is pure overhead: no process to start, no port to expose, no credentials to manage, no network round-trip for every query. It is, by shipped volume, probably the most widely deployed database engine in the world — it's embedded in every major web browser, most mobile operating systems, and countless desktop apps, usually invisibly.

The mental shift worth making: SQLite isn't "a smaller Postgres." It's a different category of tool — an embedded storage engine that happens to expose itself through SQL, aimed at a single application owning a file, not multiple applications sharing a service.

3. What it's genuinely good at

4. Where it breaks down

5. The concurrency question, in detail

This is the part that actually matters in production decisions, so it's worth being precise instead of just saying "SQLite can't handle concurrency" (which overstates the problem).

SQLite supports unlimited simultaneous readers. Where it differs from a server database is on writes: by default, only one write transaction can be in progress at any instant. If a second write comes in while the first is running, it queues and waits — it doesn't fail outright, but it does block.

In practice, this matters less than it sounds: writes in most apps are quick (a few milliseconds), so a queue of writers rarely becomes a bottleneck for low-to-moderate traffic. SQLite also ships a Write-Ahead Log (WAL) mode, which lets reads keep happening concurrently with an in-progress write and generally improves throughput over the older rollback-journal mode — most modern SQLite deployments (including popular tools like PocketBase) run in WAL mode by default for exactly this reason.

Where it actually breaks: a genuinely high write-concurrency workload — many independent processes or many parallel requests all writing at once, sustained — will start queuing noticeably and become a real bottleneck. That's the point where "just use SQLite" stops being obviously true. Newer projects like Turso have specifically tried to relax the single-writer limitation, which itself is a signal of how real the limitation is in practice.

6. How it compares to a "real" server database

DimensionSQLitePostgres / MySQL (server)
DeploymentEmbedded, single file, no server processSeparate server process, network protocol
Concurrent writersOne at a time (queued)Many, handled by the engine's concurrency control
Multi-machine accessNot supported nativelyCore use case — many app servers, one database
Setup costEffectively zeroRequires provisioning, credentials, networking
User/permission systemNone — file-level onlyFull role-based access control
Best fitSingle app owning its data, embedded/local/edge useMultiple clients or services sharing one dataset

7. A practical example

Picture a small internal tool: a single Node.js or Python process serving a handful of teammates a dashboard, backed by a few thousand rows of data that update occasionally. SQLite is a strong default here — one file, no infrastructure, trivial backups (copy the file), and the write load is low enough that the single-writer limitation never surfaces.

Now picture the same dashboard, but it grows into a multi-region SaaS product with dozens of backend instances all writing to the same dataset simultaneously, and a requirement that any instance, anywhere, can read the latest write immediately. That's the point SQLite was never designed for — the correct move is a managed server database (or one of the newer "SQLite-compatible but networked" services covered in the next lesson).

8. The rule of thumb

Use SQLite when one process (or a small number of processes on one machine) owns the data, write volume is light-to-moderate, and you want zero infrastructure. Move to a server database when multiple machines need to write concurrently, you need real user-level permissions, or sustained write concurrency starts queuing noticeably.

The official SQLite documentation puts it plainly: it's a great fit for embedded devices, local caches, prototypes, and the "device/edge" side of computing — and a poor fit for anything that looks like a shared, multi-writer client-server workload at real scale. Next lesson covers the alternatives that pick up exactly where that line gets crossed: Postgres, MySQL, Turso, and PlanetScale.

Semana 3 · Lição 13

SQLite: quando faz sentido, e quando realmente não faz

Um banco relacional completo que roda como um único arquivo, sem processo de servidor. Essa única escolha de design explica quase tudo sobre onde ele brilha e onde ele quebra.

1. O que o SQLite realmente é

Todo banco de dados que você provavelmente já ouviu falar antes deste — Postgres, MySQL, SQL Server — é um sistema cliente-servidor: um processo separado (o servidor do banco) roda em algum lugar, escuta numa porta de rede, e sua aplicação se conecta a ele como cliente, mesmo que esse servidor rode na mesma máquina.

O SQLite descarta esse modelo. Não existe processo de servidor. O "banco de dados" é só um arquivo único em disco (geralmente terminando em .db ou .sqlite), e o SQLite é uma biblioteca em C que é linkada direto no programa que quer ler ou escrever esse arquivo. Quando sua aplicação faz uma query SQL, ela não está falando com outro processo por um socket — está chamando uma função no próprio processo dela que lê e escreve o arquivo diretamente.

Ainda assim ele fala SQL de verdade (a maior parte do padrão: SELECT, JOIN, transações, índices, foreign keys) e é totalmente ACID. Não é um brinquedo nem um subconjunto "lite" de recursos relacionais — o nome se refere à natureza leve e embarcada do motor, não a um conjunto reduzido de funcionalidades.

2. Por que ele existe — e por que "banco serverless" é a forma certa de pensar nele

O SQLite foi construído pro caso em que rodar e manter um servidor de banco separado é pura sobrecarga: nenhum processo pra iniciar, nenhuma porta pra expor, nenhuma credencial pra gerenciar, nenhuma ida-e-volta de rede pra cada query. É, em volume de instalações, provavelmente o motor de banco de dados mais distribuído do mundo — está embarcado em todo navegador web importante, na maioria dos sistemas operacionais mobile, e em inúmeros apps desktop, geralmente de forma invisível.

A mudança de mentalidade que vale a pena fazer: o SQLite não é "um Postgres menor". É uma categoria diferente de ferramenta — um motor de armazenamento embarcado que por acaso se expõe via SQL, voltado a uma única aplicação que é dona de um arquivo, não a múltiplas aplicações compartilhando um serviço.

3. No que ele é genuinamente bom

4. Onde ele quebra

5. A questão da concorrência, em detalhe

Essa é a parte que realmente importa em decisões de produção, então vale ser preciso em vez de só dizer "SQLite não lida com concorrência" (o que exagera o problema).

O SQLite suporta leitores simultâneos ilimitados. Onde ele difere de um banco de servidor é na escrita: por padrão, só uma transação de escrita pode estar em andamento a cada instante. Se uma segunda escrita chega enquanto a primeira está rodando, ela entra na fila e espera — não falha de imediato, mas bloqueia.

Na prática, isso importa menos do que parece: escritas na maioria das aplicações são rápidas (poucos milissegundos), então uma fila de escritores raramente se torna um gargalo pra tráfego baixo a moderado. O SQLite também traz um modo Write-Ahead Log (WAL), que permite que leituras continuem acontecendo em paralelo com uma escrita em andamento e geralmente melhora a vazão em relação ao modo mais antigo de rollback journal — a maioria dos deploys modernos de SQLite (incluindo ferramentas populares como o PocketBase) roda em modo WAL por padrão exatamente por isso.

Onde ele realmente quebra: uma carga de trabalho com concorrência de escrita genuinamente alta — muitos processos independentes ou muitas requisições paralelas escrevendo ao mesmo tempo, de forma sustentada — vai começar a filar visivelmente e se tornar um gargalo real. É o ponto em que "só usa SQLite" deixa de ser obviamente verdade. Projetos mais novos como o Turso tentaram especificamente relaxar a limitação de escritor único, o que por si só é um sinal de quão real essa limitação é na prática.

6. Como ele se compara a um banco de servidor "de verdade"

DimensãoSQLitePostgres / MySQL (servidor)
DeployEmbarcado, arquivo único, sem processo de servidorProcesso de servidor separado, protocolo de rede
Escritores concorrentesUm por vez (em fila)Muitos, controlados pelo mecanismo de concorrência do motor
Acesso multi-máquinaNão suportado nativamenteCaso de uso central — muitos servidores de app, um banco
Custo de configuraçãoPraticamente zeroExige provisionamento, credenciais, rede
Sistema de usuários/permissõesNenhum — só nível de arquivoControle de acesso completo baseado em papéis
Melhor usoUma aplicação dona dos próprios dados, uso embarcado/local/edgeMúltiplos clientes ou serviços compartilhando um conjunto de dados

7. Um exemplo prático

Imagine uma ferramenta interna pequena: um único processo Node.js ou Python servindo um dashboard pra um punhado de colegas, com uns poucos milhares de linhas de dados que mudam ocasionalmente. O SQLite é uma escolha forte aqui — um arquivo só, sem infraestrutura, backup trivial (copiar o arquivo), e a carga de escrita é baixa o suficiente pra que a limitação de escritor único nunca apareça.

Agora imagine o mesmo dashboard, mas ele cresce pra um produto SaaS multi-região com dezenas de instâncias de backend todas escrevendo no mesmo conjunto de dados simultaneamente, e um requisito de que qualquer instância, em qualquer lugar, consiga ler a última escrita imediatamente. Esse é o ponto pro qual o SQLite nunca foi desenhado — a decisão certa é um banco de servidor gerenciado (ou um dos serviços mais novos "compatíveis com SQLite mas em rede" cobertos na próxima lição).

8. A regra prática

Use SQLite quando um processo (ou um número pequeno de processos numa única máquina) é dono dos dados, o volume de escrita é leve a moderado, e você quer infraestrutura zero. Migre pra um banco de servidor quando múltiplas máquinas precisam escrever concorrentemente, você precisa de permissões reais por usuário, ou a concorrência de escrita sustentada começa a filar visivelmente.

A documentação oficial do SQLite coloca isso de forma direta: é uma ótima escolha pra dispositivos embarcados, cache local, prototipagem, e o lado "dispositivo/edge" da computação — e uma escolha ruim pra qualquer coisa que se pareça com uma carga de trabalho cliente-servidor compartilhada, com múltiplos escritores, em escala real. A próxima lição cobre as alternativas que assumem exatamente a partir desse ponto: Postgres, MySQL, Turso e PlanetScale.