Tailscale utilisait SQLite depuis 2022 comme base principale de ses serveurs de coordination. Le choix devait être ennuyeux, au meilleur sens du terme : une base connue, locale, avec un seul processus Go par shard pour l’utiliser.1

Puis les bases ont commencé à se corrompre.

Entre l’été 2025 et le début de 2026, Tailscale dit avoir subi 19 incidents de corruption en six mois. Le problème pouvait couper le control plane d’un shard pendant la réparation, sans exposer les clés privées ni le trafic des utilisateurs, mais avec de vraies interruptions de service.1

Le plus intéressant n’est pas qu’un vieux logiciel ait eu un bug. C’est la manière dont Tailscale et les mainteneurs de SQLite ont fini par fabriquer les instruments nécessaires pour voir une course critique qui refusait d’apparaître en laboratoire.

Le bug ne ressemblait à rien de reproductible

Les incidents n’étaient liés ni à un shard, ni à un client, ni à une heure, ni à une charge particulière. Parfois deux corruptions arrivaient à quelques heures d’intervalle. Parfois rien pendant six semaines.1

Tailscale a revu son code SQLite, cherché des facteurs communs et ajouté de la télémétrie. L’entreprise a aussi pris un contrat de support avec les développeurs de SQLite. Les hypothèses ont été éliminées progressivement : verrouillage POSIX, mémoire mal gérée, accès multithread mal configuré.1

Pendant ce temps, il fallait garder le service debout. Les shards ont été configurés pour s’arrêter immédiatement en cas de corruption. Les backups ont été vérifiés automatiquement avec PRAGMA integrity_check. Les procédures de récupération ont été accélérées.1

Mais la vraie avancée est venue d’un outil construit pour un autre problème immédiat : restaurer une base sans revenir trop loin en arrière.

Le replay transactionnel est devenu un microscope

Tailscale a commencé à journaliser séparément chaque instruction SQL qui modifiait la base. Comme chaque shard utilisait un seul writer, l’historique pouvait être rejoué dans l’ordre sur le dernier backup sain.1

Le système devait d’abord réduire la perte de données. Il a surtout révélé quelque chose d’impossible : dans deux incidents, une écriture validée par une transaction devenait invisible pour les transactions suivantes.1

À partir de là, le checkpoint WAL est devenu suspect.

En mode Write-Ahead Logging, SQLite écrit d’abord les nouvelles pages dans un fichier WAL, puis les recopie périodiquement vers le fichier principal lors d’un checkpoint. Tailscale pilotait manuellement ces checkpoints pour obtenir des backups rapides et cohérents.1

SQLite documente aujourd’hui le problème comme le WAL-reset bug. Il peut se produire lorsqu’au moins deux connexions au même fichier, dans des threads ou processus distincts, écrivent et checkpointent au même moment. La fenêtre de course est très étroite.2

Tailscale utilisait justement une configuration qui augmentait les occasions de tomber dans cette fenêtre.

Il a fallu instrumenter SQLite lui-même

Les développeurs SQLite ont créé un shim de système de fichiers virtuel, tmstmpvfs, afin de tracer plus précisément ce qui se passait pendant les checkpoints. Tailscale l’a déployé en production et a attendu l’incident suivant.1

Cette fois, les traces ont isolé une course entre un checkpoint et une transaction d’écriture. Dans une séquence rare, le checkpoint pouvait croire que certaines pages du WAL avaient déjà été recopiées alors qu’elles ne l’étaient pas. Ces pages étaient perdues et le fichier principal devenait incohérent.1

La documentation SQLite indique que le bug était probablement présent depuis SQLite 3.7.0, sorti en juillet 2010, jusqu’à 3.51.2. Il a été corrigé dans 3.51.3, publié le 13 mars 2026.23

Autrement dit : presque seize ans dans SQLite, sans que les chemins ordinaires ne fassent suffisamment apparaître cette course.

La première version du correctif a créé une autre alerte

L’histoire a même offert un faux final. Tailscale raconte avoir d’abord déployé SQLite 3.52.0, qui contenait le correctif. Son moniteur de backups a alors signalé treize bases corrompues.1

Cette fois, les bases ne l’étaient pas réellement. Une modification distincte du calcul de certaines expressions avait rendu des index d’expression obsolètes et provoquait de faux diagnostics de corruption. SQLite a retiré 3.52.0 et publié 3.51.3 avec le correctif WAL-reset.13

Le changelog officiel confirme le retrait de 3.52.0 et le correctif dans 3.51.3.3

Le vrai enseignement n’est pas « n’utilisez pas SQLite »

SQLite insiste lui-même sur le caractère rare du bug. Il faut le mode WAL, plusieurs connexions au même fichier et une collision très précise entre écriture et checkpoint. Les développeurs ont dû ajouter une logique de test qui force les conditions pour reproduire le problème.2

Tailscale en tire une conclusion plus utile : une technologie « boring » peut devenir nettement moins boring dès qu’on l’utilise sur un chemin inhabituel, même si ce chemin reste documenté et supporté.1

Ce qui mérite d’être copié ici n’est donc pas leur architecture SQLite. C’est leur escalade d’outillage.

Quand les logs normaux n’ont rien montré, ils ont ajouté un contrôle d’intégrité. Quand les backups n’expliquaient rien, ils ont ajouté un journal de transactions. Quand le journal a révélé une incohérence, les mainteneurs SQLite ont instrumenté le VFS.

À chaque étape, l’outil de récupération est devenu un outil d’observation.

Un bug qui n’était presque jamais visible a fini par laisser suffisamment de traces pour être compris. C’est probablement la partie la plus réutilisable de ces six mois : quand un système refuse d’être reproductible, il faut parfois arrêter d’essayer de reproduire le bug et commencer par construire le dispositif qui saura l’attraper la prochaine fois.