ultrathink.art est une boutique e-commerce exploitée de manière autonome par des agents IA. La conception des produits, le traitement des commandes et même la rédaction du blog sont entièrement pris en charge par l’IA. Cet article raconte l’expérience vécue en faisant tourner cette boutique avec SQLite dans un environnement de production, jusqu’au traitement réel des paiements Stripe.
Configuration : 4 fichiers, 1 volume
En production, quatre bases de données SQLite sont utilisées : primary (commandes, produits, utilisateurs), cache (cache Rails), queue (jobs en arrière-plan) et cable (Action Cable). Elles sont toutes stockées sur un seul volume Docker.
Rails 8 a fait de SQLite l’option de premier choix, et en pratique cela a apporté des avantages comme un déploiement simplifié, l’absence de gestion de pool de connexions et l’absence de serveur de base de données séparé.
Pourquoi le mode WAL permet la concurrence
Le mode de journalisation par défaut de SQLite verrouille toute la base lors des écritures, ce qui le rend peu adapté aux applications web recevant beaucoup de requêtes simultanées. En mode WAL (Write-Ahead Logging), les écritures sont ajoutées à un fichier -wal séparé tandis que les lectures continuent d’utiliser le fichier principal, ce qui permet d’avoir plusieurs lectures et une écriture en parallèle. Rails 8 active le mode WAL par défaut pour SQLite.
Incident : deux commandes ont disparu
Le 4 février, 11 commits ont été poussés sur main en l’espace de deux heures. À chaque push, le déploiement blue-green de Kamal se lançait, créant une phase de chevauchement pendant laquelle l’ancien conteneur et le nouveau ouvraient simultanément le même fichier WAL. Avec 11 déploiements qui se chevauchaient, le conteneur A était en train de se vider pendant que B démarrait, puis le déploiement de C commençait avant même que B ne soit totalement prêt.
Les commandes 16 et 17 ont bien été payées sur Stripe, et l’argent a bien été débité du compte client, mais aucun enregistrement n’est resté dans la base de données. En vérifiant sqlite_sequence, le compteur auto-incrémenté pointait vers 17, alors qu’il n’y avait en réalité que 15 lignes.
Solution : ralentir le rythme des déploiements
La solution n’était pas technique, mais procédurale. Les changements liés ont été regroupés pour être déployés ensemble, et une règle interdisant les pushes rapprochés a été inscrite dans le fichier de gouvernance (CLAUDE.md) que les agents IA doivent suivre.
Ce n’est pas un problème de SQLite, mais un problème de pipeline de déploiement. PostgreSQL se connecte via des sockets TCP, donc les nouveaux conteneurs se connectent tous au même serveur de base de données et c’est le moteur qui gère l’ordre des écritures. SQLite, lui, dépend des verrous du système de fichiers sur un volume Docker partagé, et cela se casse lorsque des conteneurs se chevauchent.
sqlite_sequence : à utiliser comme outil forensique
La table sqlite_sequence est l’un des outils de débogage les plus sous-estimés de SQLite. Elle conserve la valeur auto-incrémentée maximale déjà attribuée, même si les lignes ont ensuite été supprimées. Si l’écart entre le nombre de lignes actuel et la valeur de séquence devient anormal, c’est le signal que des lignes ont été supprimées par erreur.
Les pièges dont personne ne parle
ILIKE, que les développeurs PostgreSQL utilisent par habitude, provoque une erreur de syntaxe dans SQLite. Il faut utiliser LOWER(name) LIKE à la place. json_extract peut renvoyer un entier si la valeur est stockée comme nombre, ce qui fait échouer silencieusement une comparaison avec une chaîne. kamal app exec crée un nouveau conteneur à chaque exécution ; sur un serveur avec 2 Go de RAM, l’exécuter deux fois en même temps suffit à faire intervenir l’OOM killer, qui tue le processus web.
Si c’était à refaire, est-ce que je choisirais encore SQLite ?
Oui. Sur un serveur unique avec une charge d’écriture raisonnable, SQLite élimine d’un coup toute la complexité de l’infrastructure. Pour les sauvegardes, une seule commande sqlite3 .backup suffit (elle gère en toute sécurité le mode WAL et les écritures concurrentes). Le jour où il faudra de l’extension horizontale ou une vraie concurrence multi-writer, il sera temps de migrer vers PostgreSQL. Rails rend cette transition simple.
Source originale : ultrathink.art Blog, 2026.04.03
4 commentaires
Même s’il y a des problèmes de complexité d’infrastructure, dans un contexte comme un site e-commerce où il faut beaucoup d’écritures concurrentes, n’est-ce pas un meilleur choix de ne pas utiliser sqlite dès le départ ?
Et si c’était en plus déployé avec Docker, la complexité d’infrastructure d’une configuration postgresql ne serait pas non plus si élevée.
J’ai l’impression que, puisque c’est fait avec rails et que l’écosystème est orienté vers l’utilisation de sqlite, on a été incité à penser que c’était une bonne idée.
Une erreur grave comme la perte de commandes s’est même produite, et dans un contexte comme un site e-commerce où il faut beaucoup d’écritures concurrentes, n’est-ce pas un meilleur choix de ne pas utiliser sqlite dès le départ, même s’il y a des problèmes d’écritures concurrentes avec pg ?
Comme c’est fait avec rails et que l’écosystème est structuré autour de l’usage de sqlite, j’ai aussi l’impression qu’on a été amené à penser que c’était une bonne solution.
Une erreur majeure comme la perte de commandes s’est produite, et la manière fondamentale de résoudre cela est d’utiliser une base de données gérant les écritures concurrentes comme pg.
Comme il y a une préférence technique pour sqlite, s’obstiner à dire qu’on va continuer à l’utiliser me paraît, en tant qu’ingénieur, être une déclaration qui entame ma confiance.
J’ai l’impression que c’est la version inverse du développement « résumé-driven », où l’on déploie k8s sans besoin réel, configure un HPA avec un seul replica, et transforme un monolithe qui fonctionnait bien en MSA.
Le problème vient d’une mauvaise configuration du volume au démarrage du conteneur, pas du fait que les écritures concurrentes ne soient pas prises en charge. Si vous relisez l’article, il est indiqué que l’absence d’écritures concurrentes est largement compensée par le
busy timeout. La configuration du volume peut être résolue avecipc=host.Postgres doit au final être exploité en production, alors qu’avec sqlite, il suffit de déployer le binaire de l’application n’importe où.
À force d’accumuler d’innombrables expériences d’exploitation et échecs, sqlite commence à gagner en popularité, et l’article indique clairement que les écritures concurrentes ne posent absolument pas problème.
En fait, il suffit de faire plusieurs réglages, mais au final, l'expérience reste indispensable, héhé. En tout cas, moi, ce genre d'article, j'aime bien.
Oui, justement, c’est d’autant plus agréable de voir ce genre de billet apparaître de temps en temps.