2026-07-02 · 7 min
SQLite vs Postgres SaaS 1k MAU: 6 Bulan Real Data
Enam bulan lalu, saya migrate side project SaaS invoice — 1.200 MAU, ~95 RPS peak — dari Postgres ke SQLite + Litestream. Sebagian dari saya skeptis (“SQLite cuma untuk embedded”). Sebagian dari saya butuh hemat. Setelah 6 bulan, share angka real.
Sebelumnya saya sudah catat sedikit di SQLite Litestream small SaaS — tulisan ini lebih dalam, dengan angka Postgres comparison.
Konteks SaaS
- App: SaaS invoice freelancer Indonesia (saya pribadi, paying customer subscription Rp 49k-249k/bulan).
- MAU: 1.247 (bulan ke-6, growth ~8% MoM).
- DAU: ~340 average, ~580 peak (akhir bulan: tagihan).
- Data size: 4.8GB (50k invoice, 12k klien, 380k line item).
- Write rate: peak 18 write/detik (akhir bulan), avg 4 write/detik.
- Read rate: peak 95 read/detik.
Saya bukan startup VC-funded — saya freelance yang bangun SaaS sambilan. Cost optimization matter.
Setup awal: Postgres
- Hetzner CCX23 (4 vCPU dedicated, 16GB RAM, 240GB NVMe) — €27/bulan ≈ Rp 480k.
- PgBouncer transaction mode, 50 connection pool.
- Daily logical backup ke S3-compatible (Backblaze B2).
Performance baseline (4 bulan Postgres production):
- P50 query: 1.8ms
- P95 query: 8.2ms
- P99 query: 32ms (cold cache atau index miss)
- Connection wait time p95: 4ms
- Backup time daily: 6 menit
Cost komponen:
- VPS: Rp 480k/bulan
- B2 backup storage (60 hari rotation): Rp 22k/bulan
- Total: Rp 502k/bulan
Migrasi ke SQLite + Litestream
Hipotesis: 95 RPS bisa di-handle SQLite di VPS yang jauh lebih kecil.
Setup baru:
- Hetzner CX22 (2 vCPU shared, 4GB RAM, 40GB disk) — €5.4/bulan ≈ Rp 95k.
- SQLite 3.46 dengan WAL mode,
synchronous=NORMAL,cache_size=-64000(64MB cache). - Litestream replicate ke Backblaze B2 (continuous, 10 detik interval).
- App: Bun + Hono + better-sqlite3.
SQLite pragma:
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -64000;
PRAGMA temp_store = MEMORY;
PRAGMA mmap_size = 268435456;
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
Litestream config:
dbs:
- path: /var/lib/kami/invoice.db
replicas:
- type: s3
bucket: kami-litestream
path: invoice
endpoint: s3.us-west-002.backblazeb2.com
access-key-id: ${B2_KEY_ID}
secret-access-key: ${B2_APP_KEY}
retention: 720h # 30 hari
snapshot-interval: 24h
sync-interval: 10s
Migrasi data: dump dari Postgres, transform schema (jsonb → json, ENUM → TEXT CHECK), import ke SQLite. Total: 1 jam 40 menit untuk 4.8GB.
Performance setelah 6 bulan
Saya pakai metric yang sama, identical workload (Production traffic).
| Metric | Postgres | SQLite | Delta |
|---|---|---|---|
| P50 query | 1.8ms | 0.12ms | -93% |
| P95 query | 8.2ms | 0.8ms | -90% |
| P99 query | 32ms | 4.1ms | -87% |
| Write p95 (insert + commit) | 6.4ms | 1.2ms | -81% |
| Concurrent write throughput max | ~800/s | ~180/s | -78% |
| Connection wait p95 | 4ms | 0ms (no pool) | - |
| Memory usage | 2.4GB | 280MB | -88% |
| Backup latency (point-in-time) | 24h | 10s | huge |
SQLite menang besar di latency karena in-process — no network roundtrip, no connection pool overhead. Postgres menang di concurrent write (yang saya tidak butuh).
Memory turun 88% karena Postgres punya shared_buffers, work_mem, autovacuum worker, dll. SQLite cuma cache page + mmap.
Cost 6 bulan
| Item | Postgres | SQLite |
|---|---|---|
| VPS | 6 × Rp 480k = Rp 2.88 juta | 6 × Rp 95k = Rp 570k |
| Backup storage | 6 × Rp 22k = Rp 132k | 6 × Rp 18k = Rp 108k |
| Total 6 bulan | Rp 3.01 juta | Rp 678k |
Saving: Rp 2.33 juta / 6 bulan. Untuk solo SaaS dengan revenue Rp 12 juta/bulan: signifikan.
Disaster recovery test
Saya test 2 kali dalam 6 bulan:
Test 1: Simulated VPS loss
Saya rm -rf /var/lib/kami/invoice.db. Restore via Litestream:
litestream restore -if-replica-exists -o /var/lib/kami/invoice.db \
s3://kami-litestream/invoice
Restore time untuk 4.8GB: 4 menit 12 detik. Data loss: 0 (Litestream sync interval 10s, last sync 3 detik sebelum delete).
Test 2: Corruption recovery
Saya corrupt file SQLite manual (dd if=/dev/urandom of=invoice.db count=1 bs=1024 seek=100). App crash. Restore via Litestream.
Restore time: 4 menit 8 detik. Data loss: 8 invoice yang di-write dalam 10 detik sebelum corruption (Litestream belum sync).
RPO 10 detik, RTO 5 menit. Untuk SLA 99.9% bulanan (43 menit downtime budget), acceptable.
Yang break
1. Concurrent write contention
Saat fitur “bulk import” diaktifkan (CSV upload, ~500 invoice per file), beberapa user trigger bersamaan. SQLite serialize write — user kedua wait ~12 detik kalau import pertama besar.
Fix: queue write besar ke background worker (BullMQ-like di Bun), respond ke user “import in progress, akan email saat selesai”.
Postgres bisa handle ini natively dengan multiple connection. Trade-off saya: accept queue UX untuk save Rp 380k/bulan.
2. Full-text search adoption
Postgres FTS dengan tsvector powerful. SQLite FTS5 OK tapi lebih basic.
Saat user request “search invoice by line item description”, saya butuh FTS. SQLite FTS5 setup:
CREATE VIRTUAL TABLE invoice_fts USING fts5(
invoice_id UNINDEXED,
description,
content='line_items',
content_rowid='id'
);
CREATE TRIGGER line_items_ai AFTER INSERT ON line_items BEGIN
INSERT INTO invoice_fts(rowid, invoice_id, description)
VALUES (new.id, new.invoice_id, new.description);
END;
Works untuk Bahasa Indonesia search query simple. Tidak ada stemming bahasa Indonesia built-in (Postgres ada via unaccent + custom dictionary). Saya hidup tanpa stemming — user feedback ngga complain.
3. Backup verify yang tidak pernah saya jalankan
Saya backup tapi tidak pernah verify integritas backup sampai test 1 dan 2 di atas. Itu malpraktek. Sekarang saya jalankan automated restore test mingguan ke staging:
#!/bin/bash
# weekly-restore-test.sh
set -e
litestream restore -o /tmp/test-restore.db s3://kami-litestream/invoice
sqlite3 /tmp/test-restore.db "PRAGMA integrity_check;" | grep -q "ok" || exit 1
sqlite3 /tmp/test-restore.db "SELECT COUNT(*) FROM invoice;" | tee /tmp/count.txt
rm /tmp/test-restore.db
# Notify Slack
curl -X POST $SLACK_WEBHOOK -d "{\"text\":\"Litestream restore OK, $(cat /tmp/count.txt) invoice\"}"
4. Analytics query yang lambat
Akhir bulan, saya butuh report “total revenue per tier subscriber dengan growth MoM, breakdown by region”. Query Postgres dengan window function: 240ms. SQLite same query: 1.8 detik.
Untuk report internal, OK. Kalau saya butuh dashboard live dengan analytics berat, saya akan butuh datawarehouse terpisah (DuckDB? ClickHouse?). Sekarang saya jalankan report sekali sehari via cron, cache hasil. Tidak masalah.
Kapan saya tetap pilih Postgres
- Multi-region active-active (SQLite tidak bisa)
- Concurrent write > 200/detik (SQLite topi ~180/detik di VPS modest)
- Complex window function / analytical query (Postgres planner lebih mature)
- Team familiar Postgres dan tidak punya bandwidth belajar Litestream
- Compliance yang butuh row-level security (Postgres native, SQLite manual)
- Heavy JSON workload (Postgres jsonb path operator vs SQLite json_extract)
Verdict
Untuk solo / micro SaaS Indonesia dengan < 5k MAU dan write < 50/detik: SQLite + Litestream cocok, dan menghemat 75-85% cost database tier vs Postgres VPS.
Untuk SaaS 10k+ MAU atau multi-region: Postgres still default.
Yang penting bukan “SQLite vs Postgres” tapi “apa profil traffic Anda dan apa budget Anda”. Saya tidak akan migrate balik ke Postgres sampai MAU saya nyentuh 5-10k atau saya butuh feature yang SQLite tidak punya.
Bukan magic. SQLite di production butuh disiplin (backup, integrity check, monitoring). Tapi untuk profile kerjaan saya: cocok. Lihat juga Cloudflare D1 vs Postgres warung untuk konteks SQLite di edge.
Ditulis oleh Reza Pradipta