Bug SQLite 16 Tahun dan Batas Teknologi 'Boring'

Bug SQLite 16 Tahun dan Batas Teknologi 'Boring'
Sebuah bug bersembunyi di SQLite selama enam belas tahun. Kerjanya diam-diam menghapus baris data yang sudah commit, tanpa error, tanpa crash. Penyebabnya bukan SQLite jelek — melainkan Tailscale memakainya dengan cara yang sedikit menyimpang dari kebiasaan mayoritas pengguna lain.
Agustus 2026, Tailscale merilis post-mortem yang butuh enam bulan untuk ditulis. Enam bulan itu habis untuk mencari tahu apa yang diam-diam menghapus baris data dari database mereka. Sembilan belas insiden korupsi data terpisah, tanpa pemicu yang konsisten, tanpa pesan error. Bug-nya bukan di kode Tailscale. Bug itu ada di dalam SQLite sendiri — database yang dibundel ke lebih banyak perangkat dan aplikasi dibanding hampir semua software lain. Bug itu sudah bersembunyi di sana selama kurang lebih enam belas tahun.
Tulisannya mencapai 1.221 poin dan 239 komentar di Hacker News. Hampir semua engineer punya SQLite di salah satu sudut stack-nya, dan mekanisme bug-nya memunculkan pertanyaan yang wajar: apakah ini berlaku untuk sistem saya juga?
Apa yang sebenarnya rusak
Bayangkan kurir yang menandai paket "terkirim" di sistem, padahal ia baru meletakkannya di truk dan belum benar-benar menurunkannya. Kalau truk itu berbelok tepat di detik yang salah, sistem sudah telanjur bilang selesai — padahal paketnya masih di jalan. Bug ini kerjanya mirip persis.
Mode default SQLite, write-ahead logging (WAL), tidak menulis langsung ke file database utama. Halaman data baru masuk dulu ke file WAL terpisah; sebuah "checkpoint" belakangan menyalin halaman-halaman itu ke file utama dan me-reset log-nya. Langkah checkpoint inilah tempat bug-nya bersembunyi — data race antara checkpoint yang menyalin halaman WAL dan write transaction yang commit halaman baru.
Ketika timing-nya jatuh di saat yang salah, checkpoint menyimpulkan sudah menyalin semua halaman, padahal satu atau lebih halaman yang sudah commit tidak pernah benar-benar tersalin. Transaksinya melaporkan sukses ke pemanggil. Datanya hilang.
Pemicunya spesifik: database mode WAL dengan dua koneksi atau lebih — thread atau proses terpisah. Kedua koneksi itu menulis atau checkpoint pada file yang sama di saat yang sama persis. Dokumentasi SQLite sendiri mengonfirmasi ini adalah data race dengan batas timing yang sangat ketat. Kalau kombinasi itu tidak pernah terjadi, bug ini tidak akan pernah muncul. Kalau sering terjadi, seperti di skala Tailscale, hasilnya sembilan belas insiden dalam enam bulan.
Kenapa enam belas tahun pemakaian produksi tidak menemukannya
Bug ini kemungkinan ada sejak versi 3.7.0, rilis Juli 2010, sampai 3.51.2, rilis Januari 2026. Baru diperbaiki mulai 3.51.3, menurut dokumentasi resmi SQLite. Enam belas tahun, di database yang mungkin paling banyak dipakai di dunia, dan tidak ada yang menemukannya.
Ini bukan aib buat tim engineering SQLite. Ini fakta tentang apa yang sebenarnya dibuktikan oleh "battle-tested". Mayoritas besar deployment SQLite pakai satu koneksi per proses, checkpoint mengikuti jadwal default SQLite, jauh dari race window.
Pola pemakaian Tailscale berbeda: checkpoint agresif dan manual, lintas banyak koneksi konkuren sekaligus, demi memeras throughput lebih tinggi dari SQLite dibanding asumsi default-nya. Di titik itulah "semua orang sudah menemukan bug-nya" berhenti berlaku, karena tidak ada orang lain yang melakukan hal persis sama.
Antithesis, perusahaan deterministic testing, berhasil mereproduksi bug ini dalam sekitar lima belas menit, memakai workload generik write-dan-checkpoint konkuren. Ini bug yang bahkan developer SQLite sendiri tidak pernah berhasil memicu secara organik. Bukan edge case eksotis. Ini bentuk workload biasa yang nyaris tidak ada yang kebetulan membuatnya, sampai seseorang sengaja melakukannya.
Pelajaran sebenarnya bukan soal SQLite
Saran untuk memilih teknologi boring dan matang dibanding yang baru dan trendi masih tetap benar. Saya kasih saran itu terus-menerus. Tapi "boring" adalah klaim tentang pola pemakaian tertentu, bukan tentang software-nya secara abstrak.
Reputasi battle-tested sebuah dependency lahir dari akumulasi pemakaian orang lain, dan reputasi itu berlaku hanya sejauh pemakaiannya tumpang tindih dengan pemakaian mereka. Begitu keluar dari asumsi default, sebuah tim diam-diam sudah keluar dari testing kolektif yang membuat pilihan itu terasa aman.
Saya paling sering melihat ini di sistem embedded dan edge, tempat tim mendorong satu tool yang sudah familiar ke peran yang tidak pernah diantisipasi default-nya. Konkurensi lebih berat, timing lebih ketat, jalur recovery yang tidak biasa — semua penyimpangan dari asumsi awal. Setiap penyimpangan itu tidak terlihat saat code review — kelihatannya cuma pemakaian normal dari tool yang sudah dipercaya.
Korupsi diam adalah mode kegagalan terburuk
Ini yang harusnya lebih mengkhawatirkan dibanding bug-nya sendiri: tidak ada satu pun yang berisik. Tidak ada crash, tidak ada exception, tidak ada baris log. Write transaction melaporkan sukses, checkpoint selesai tanpa keluhan, dan satu-satunya gejala adalah data yang seharusnya ada tapi tidak ada. Gejala itu baru ditemukan belakangan, secara tidak langsung, oleh seseorang yang menyadari satu baris hilang.
Database yang menolak sebuah write, atau proses yang crash karena halaman rusak, memang menyebalkan tapi jujur. Keduanya memberi tahu ada yang salah saat itu juga. Yang melapor sukses sambil diam-diam kehilangan data tidak memberi apa pun untuk direspons.
Untuk deployment edge tanpa DBA yang memantau dashboard, jarak antara "kelihatannya baik-baik saja" dan "benar-benar baik-baik saja" bisa berjalan berbulan-bulan. Persis seperti yang terjadi selama enam bulan di Tailscale, perusahaan yang justru sedang mencarinya dengan sengaja.
Artinya buat SQLite di edge
Semua ini tidak membuat SQLite jadi pilihan buruk untuk deployment IoT, embedded, atau edge. SQLite tetap pilihan default yang tepat untuk workload lokal, single-process, single-writer, yang mencakup mayoritas deployment.
Tapi batas di sekitar asumsi mode WAL sekarang lebih tegas. Dokumentasi SQLite menyatakan mode WAL tidak bekerja lewat network filesystem, karena butuh semua proses yang mengakses database berbagi shared memory di host yang sama. File database di storage jaringan atau shared sudah salah arsitektur sejak sebelum bug ini ada.
Risiko yang lebih sempit adalah write konkuren multi-koneksi dengan kontrol checkpoint manual — persis pola Tailscale. Disiplin single-writer, satu proses, satu koneksi, jadwal checkpoint default, menjaga sistem tetap aman dari race condition yang dibutuhkan bug ini. Banyak proses yang menghantam file WAL yang sama, dengan interval checkpoint yang diatur manual, adalah penyimpangan yang layak diaudit sekarang. Itu berlaku di setiap versi yang lebih lama dari 3.51.3.
Deteksi korupsi murah yang harusnya sudah dimiliki setiap tim
Tidak perlu mereproduksi race condition berusia enam belas tahun untuk melindungi diri dari kategori kegagalan ini. Beberapa kebiasaan murah menangkap korupsi diam jauh sebelum jadi misteri:
- Jalankan
PRAGMA integrity_check(atau versi lebih cepat,quick_check) secara terjadwal. Perintah ini melakukan pemindaian konsistensi level rendah secara penuh — entri yang salah format, halaman hilang, index rusak. Ia mengembalikan satu barisokkalau tidak ada masalah. Nyaris gratis untuk dijadwalkan, dan langsung menangkap kelas masalah ini. - Verifikasi backup dengan cara me-restore-nya. Backup yang belum pernah di-restore adalah harapan, bukan rencana. Saya bahas prinsip yang sama di verifikasi backup Postgres; prinsipnya berlaku persis sama untuk file SQLite yang tersimpan di perangkat embedded.
- Pantau divergensi diam, bukan cuma uptime. Row count atau checksum terhadap sumber sekunder memunculkan data yang diam-diam hilang jauh lebih cepat dibanding tiket support mana pun.
Tidak satu pun dari ini butuh tooling baru, cuma keputusan untuk memberi integritas data perhatian monitoring yang sama seperti latency dan error rate.
Yang perlu dibawa pulang para pemimpin engineering
Dependency yang paling keras diaudit biasanya yang paling baru. Yang tidak pernah diperiksa ulang justru yang sudah terpasang bertahun-tahun, dibiarkan begitu saja karena "boring" dan "selalu bekerja". Itu terbalik: kadar audit harusnya sebanding dengan seberapa jauh pemakaiannya menyimpang dari default sebuah dependency, bukan sebanding dengan seberapa baru dependency itu.
Ketika saya mengambil alih sebuah sistem untuk klien — latihan yang sama seperti yang saya jelaskan di menstabilkan codebase warisan — pertanyaan pertama saya selalu sama. Bagian battle-tested mana yang dipakai dengan cara yang tidak biasa? Bukan karena tool-nya patut dicurigai. Tapi karena "battle-tested" hanya melindungi dari pertempuran yang sudah orang lain lalui dengan cara pemakaian yang sama.
Artikel Terkait
Mau bangun hal serupa?
Jasa Pengembangan Backend IoT & Integrasi Multi-Protokol
Backend yang menyerap telemetri perangkat lintas MQTT, WebSocket, Modbus, dan BLE, lalu menyatukannya jadi dashboard real-time yang andal.
Lihat cara saya bisa bantu