# Menembus Batas SQLite di Production: Mitos, Konfigurasi WAL, dan Skala Web Modern

> Diterbitkan oleh Literally Web ID pada 21 Oct 2026
> Sumber: https://literally.web.id/articles/menembus-batas-sqlite-di-production-mitos-konfigurasi-wal-dan-skala-web-modern

---

Dalam 15 menit ke depan, kita akan merakit sebuah stack berbasis SQLite yang bukan main-main—mengubah database file tunggal ini menjadi mesin berperforma tinggi yang mampu melayani lalu lintas web menengah-besar tanpa kedip. Lupakan sejenak dogma lama bahwa SQLite hanya cocok untuk aplikasi *desktop* atau *staging*; di tangan arsitek yang paham cara menjinakkan *concurrency*, SQLite justru memangkas kompleksitas infrastruktur, menghemat biaya S3/RDS, dan terbang secepat kilat karena data berada di memori dan *filesystem* lokal.

Bayangkan kita sedang berada di ruang mesin sebuah kapal tanker digital. Selama bertahun-tahun, industri di Doktrin MySQL/PostgreSQL mengajarkan kita bahwa setiap aplikasi web butuh server database terpisah. Padahal, untuk sebagian besar arsitektur modern berbasis Go atau Node.js yang dibungkus Nginx dan ditaruh di balik Cloudflare Edge, *network hop* menuju database eksternal justru menjadi *bottleneck* terbesar. Hari ini, kita akan meracik SQLite agar bernapas lega di production, mengaktifkan mode WAL, menjinakkan *concurrent writes*, dan merancang sistem *backup* otomatis tanpa butuh server *replica* terpisah.

Sebelum kita mulai menyalakan terminal, pastikan meja kerja digital kita bersih dan siap. Kita butuh environment Linux (Ubuntu/Debian atau Alpine), web server Nginx, runtime bahasa pilihan kita (misalnya Go atau Node.js dengan driver SQLite yang mendukung WAL), serta utilitas `sqlite3` CLI untuk memverifikasi PRAGMA. Jika semuanya sudah terpasang, mari kita ambil secangkir kopi dan mulai merakit.

Perjalanan kita dimulai dengan memetakan apa yang sebenarnya terjadi di balik layar ketika kita memanggil SQLite di production. Secara default, SQLite menggunakan mode *journaling* klasik yang mengunci seluruh database saat ada proses *write*, membuat proses *read* harus antre dan memicu error `database is locked` jika trafik melonjak. Di sinilah arsitektur kita berubah. Kita akan mengaktifkan mode **Write-Ahead Logging (WAL)**, menyetel `busy_timeout` agar koneksi sabar menunggu alih-alih langsung lempar *exception*, dan mengatur ukuran *cache* PRAGMA agar muat di RAM server kita.

> [!IMPORTANT]
> Jangan pernah menjalankan SQLite di production tanpa menyetel `PRAGMA journal_mode=WAL;` dan `PRAGMA synchronous=NORMAL;`. Kombinasi ini adalah garis pertahanan pertama antara aplikasi yang stabil dan aplikasi yang hancur karena *disk I/O bottleneck*.

Mari kita langsung tuliskan konfigurasi koneksi database di aplikasi backend kita. Sebagai contoh, kita gunakan Go dengan driver `mattn/go-sqlite3` atau `modernc.org/sqlite`:

```go
package main

import (
	"database/sql"
	"fmt"
	"log"
	_ "github.com/mattn/go-sqlite3"
)

func InitDB(filepath string) (*sql.DB, error) {
	dsn := fmt.Sprintf("file:%s?_journal_mode=WAL&_busy_timeout=5000&_synchronous=NORMAL", filepath)
	db, err := sql.Open("sqlite3", dsn)
	if err != nil {
		return nil, err
	}

	// Batasi open connection agar tidak terjadi perebutan write lock yang berlebihan
	db.SetMaxOpenConns(1)
	
	return db, nil
}
```

Perhatikan baris `db.SetMaxOpenConns(1)`. Ini adalah rahasia dapur para senior engineer. SQLite dirancang untuk menangani banyak pembaca (*readers*), tetapi hanya **satu penulis aktif** (*writer*) dalam satu waktu. Dengan membatasi koneksi tulis maksimal 1, kita mencegah *thread pool* aplikasi kita saling berebut kunci eksklusif yang berujung pada deadlock aplikasi.

Sekarang database kita sudah bernapas lega dengan WAL, tantangan berikutnya di dunia nyata adalah bagaimana menangani lonjakan *concurrent writes* dari ribuan request HTTP secara bersamaan tanpa membuat user mengalami *timeout*. Di sinilah kita mengombinasikan antrean aplikasi (*queue*) ringan di level memori dengan mekanisme *retry* yang elegan.

> [!TIP]
> Jika aplikasi Node.js atau Go Anda menerima 500 POST request per detik, jangan tembakkan semuanya langsung ke SQLite. Gunakan *write-batching* atau *channel buffer* di level aplikasi untuk menggabungkan beberapa *insert* menjadi satu transaksi tunggal.

Berikut adalah cuplikan bagaimana kita menangani antrean tulis secara aman di backend Node.js menggunakan `better-sqlite3` yang terkenal sangat cepat dan sinkron:

```javascript
const Database = require('better-sqlite3');
const db = new Database('production.db', { verbose: console.log });

// Konfigurasi WAL dan performa via PRAGMA
db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
db.pragma('busy_timeout = 5000');

// Siapkan prepared statement untuk performa maksimal
const insertStmt = db.prepare('INSERT INTO analytics (event, payload, created_at) VALUES (?, ?, ?)');

// Bungkus dalam transaksi batch untuk efisiensi luar biasa
const insertMany = db.transaction((events) => {
  for (const event of events) {
    insertStmt.run(event.name, JSON.stringify(event.payload), Date.now());
  }
});

// Contoh simulasi menerima data dari webhook / traffic web
function handleIncomingTraffic(batchOfEvents) {
  try {
    insertMany(batchOfEvents);
  } catch (err) {
    console.error('Gagal menulis ke SQLite, masuk ke mekanisme retry:', err.message);
    // Implementasikan exponential backoff di sini
  }
}
```

Dengan pola *transaction batching* seperti ini, operasi tulis yang tadinya memakan ratusan siklus *disk I/O* kini diringkus menjadi satu kali tulis di akhir transaksi WAL. Hasilnya? Throughput melonjak drastis, menyamai performa database klien-server konvensional tanpa kerumitan mengelola daemon terpisah.

Satu ketakutan terbesar para pengelola infrastruktur saat memindahkan beban kerja ke SQLite adalah: *“Bagaimana cara backup-nya tanpa mengunci database atau merusak file yang sedang aktif dibaca-tulis?”* Di sinilah keindahan arsitektur SQLite bersinar terang. Karena SQLite hanya berupa satu file, kita tidak perlu skrip `mysqldump` yang rumit atau *storage snapshots* yang mahal.

Kita bisa memanfaatkan SQLite Online Backup API atau cukup menyalin file WAL dan database secara aman menggunakan perintah *vacuum into* atau utilitas CLI resmi tanpa mematikan aplikasi web kita sedikt pun. Mari kita buat skrip *backup* otomatis yang dipadukan dengan Cron dan dikirim langsung ke S3 / MinIO storage:

```bash
#!/bin/bash
# /usr/local/bin/sqlite-backup.sh

DB_PATH="/var/www/html/production.db"
BACKUP_DIR="/var/backups/sqlite"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="$BACKUP_DIR/production_$TIMESTAMP.db"

# Pastikan direktori backup ada
mkdir -p $BACKUP_DIR

# Gunakan .backup command dari SQLite CLI untuk menyalin data secara konsisten 
# meskipun sedang ada transaksi aktif di mode WAL
sqlite3 "$DB_PATH" ".backup '$BACKUP_FILE'"

# Kompres file backup untuk menghemat ruang disk
gzip "$BACKUP_FILE"

# Kirim ke S3 kompatibel storage (misal: MinIO / AWS S3) via AWS CLI
aws s3 cp "$BACKUP_FILE.gz" s3://my-production-backups/sqlite/ --storage-class STANDARD_IA

# Bersihkan backup lokal yang berumur lebih dari 7 hari
find $BACKUP_DIR -type f -mtime +7 -exec rm {} \;

echo "[$(date)] Backup SQLite sukses dan terkirim ke cloud storage."
```

Daftarkan skrip di atas ke dalam Cron (`crontab -e`) agar berjalan otomatis setiap jam atau setiap hari:
```cron
0 */3 * * * /usr/local/bin/sqlite-backup.sh >/dev/null 2>&1
```

Sekarang, mari kita buktikan bahwa seluruh racikan arsitektur ini benar-benar hidup dan berjalan mulus di server kita. Jalankan perintah verifikasi kilat berikut di terminal untuk memastikan mode WAL aktif dan tidak ada korupsi data:

```bash
sqlite3 /var/www/html/production.db "PRAGMA journal_mode; PRAGMA synchronous; PRAGMA integrity_check;"
```

Jika terminal mengembalikan output:
```text
wal
1
ok
```
Itu adalah tanda sah bahwa sistem kita sudah siap, stabil, dan kebal terhadap lonjakan trafik web modern. Anda baru saja memangkas biaya infrastruktur bulanan Anda secara signifikan tanpa mengorbankan reliabilitas.

Terkadang, di lapangan, Anda mungkin akan menemui kendala kecil seperti error `SQLITE_BUSY: database is locked`. Jangan panik. Ini bukan berarti database Anda rusak, melainkan tanda bahwa batas waktu tunggu (`busy_timeout`) terlampaui karena ada satu transaksi berat yang memakan waktu terlalu lama. Solusinya sederhana: periksa kembali kueri yang tidak menggunakan indeks (*missing index*) sehingga melakukan *full table scan*, dan pastikan Anda tidak menjalankan operasi migrasi skema yang panjang secara bersamaan dengan lalu lintas HTTP yang padat. Sesuaikan `busy_timeout` menjadi `10000` (10 detik) jika aplikasi Anda padat transaksi.

Dengan konfigurasi ini, Anda kini memegang kendali penuh atas salah satu teknologi database paling efisien di dunia. Selamat meracik di production, dan nikmati infrastruktur web yang bersih, cepat, dan mandiri!