Аннотация

SQLite выбирают не потому, что он быстрый, а потому, что его не надо администрировать: база — это файл, движок — библиотека в процессе приложения. Из этого следуют и все ограничения, но не те, которые обычно обсуждают. Разговор про SQLite в продакшене почти всегда сводится к «одна запись за раз», хотя настоящие отказы приходят из трёх других мест: чекпойнт, который не может завершиться из-за долгого читателя; транзакция, начатая как читающая и не сумевшая стать пишущей; и файл, лежащий на файловой системе, которая не даёт обещанных блокировок.

Обзор собран по первичным источникам: документация sqlite.org (WAL, блокировки, транзакции, пределы, способы испортить базу, работа по сети, pragma, C-интерфейс чекпойнта), release notes 3.54.0 и 3.54.1, статья PVLDB 2022 года о внутренностях SQLite, документация Django, Python и Rails по работе с SQLite, документация Litestream, LiteFS и Cloudflare D1, документ экспериментальной ветки begin-concurrent и один независимый бенчмарк. Данные на 2026-10-25; собственных замеров в обзоре нет.

Главный вывод: предел SQLite — не пропускная способность, а дисциплина эксплуатации. При правильных pragma, коротких транзакциях и одном писателе он держит десятки тысяч транзакций в секунду1; при долгом читателе и BEGIN DEFERRED он начинает возвращать SQLITE_BUSY задолго до исчерпания диска или CPU.

Введение

Актуальность. SQLite перестал быть «базой для прототипов»: на нём работают сайты, edge-платформы и мобильные приложения, а вокруг выросли инструменты репликации — Litestream, LiteFS, D1. Одновременно выяснилось, что операционные границы этой модели описаны гораздо хуже, чем возможности: документация честно перечисляет ограничения, но не связывает их в картину отказов.

Цель — разобрать механику: что именно делает WAL, почему писатель один, где кончается файл и как устроены бэкап и репликация. Границы. Это не сравнение SQLite с PostgreSQL по бенчмаркам и не руководство по ORM: рассматривается движок, файл и операционные процедуры вокруг них. Академическая литература по SQLite тонка — статья PVLDB 2022 года остаётся основной работой, где авторы из SQLite и University of Wisconsin-Madison измеряют движок изнутри1.

1. Где SQLite действительно стоит

Документация формулирует позицию без маркетинга: SQLite не конкурирует с клиент-серверными СУБД, он конкурирует с fopen()2. Отсюда и границы применимости: по оценке самой команды, сайт с менее чем 100 тыс. обращений в день укладывается в SQLite с запасом, а десятикратный объём уже требует проверки; sqlite.org в 2015 году обслуживал 400–500 тыс. HTTP-запросов в день, из которых 15–20% были динамическими страницами примерно по 200 SQL-запросов, на одной виртуальной машине2.

Цифры производительности тоже известны. В статье PVLDB авторы прогнали TATP — облегчённый OLTP-бенчмарк с 80% чтений и 20% записей — и получили для SQLite в режиме WAL порядка 10 тыс. транзакций в секунду на облачном сервере: это в 10 раз больше, чем у DuckDB, на маленькой базе и в 500 раз — на большой1. На Raspberry Pi 4 разрыв скромнее (в 2 и в 60 раз), но порядок тот же, и авторы отдельно отмечают, что пропускная способность SQLite почти не зависит от размера базы, тогда как DuckDB деградирует по мере роста данных1. В тексте статьи есть и общая оценка: SQLite выдерживает «десятки тысяч транзакций в секунду», а на blob-данных в некоторых случаях работает на 35% быстрее и занимает на 20% меньше места, чем файловая система1.

Это важно для калибровки: узкое место — не количество запросов, а количество одновременных писателей и поведение чекпойнтера. Первое ограничение честно названо в документации: «SQLite поддерживает неограниченное число одновременных читателей, но только одного писателя в любой момент времени»2. Второе и третье в одном предложении не формулируются и разбираются ниже.

2. Что даёт WAL: три операции вместо двух

По умолчанию SQLite использует rollback journal: перед изменением страницы её исходное содержимое копируется в журнал, изменения пишутся в основной файл, а коммит наступает в момент удаления журнала3. WAL переворачивает схему: исходное содержимое остаётся в файле базы, а изменения дописываются в отдельный -wal, и коммит — это специальная запись в конце лога4. Авторы PVLDB описывают разницу как появление третьей примитивной операции: в режиме rollback их две — чтение и запись, в режиме WAL их три — чтение, запись и чекпойнтинг1.

Практическая выгода не в «скорости» вообще, а в двух конкретных свойствах. Первое: читатели не блокируют писателя и писатель не блокирует читателей, потому что каждый читатель запоминает позицию последней валидной commit-записи — свой «end mark» — и видит базу ровно на этот момент4. Второе: запись становится последовательной, а fsync исчезает с пути коммита, если разрешено пожертвовать долговечностью4.

Цена тоже конкретна. Все процессы, работающие с базой в WAL, должны быть на одной машине: читателям нужен wal-index в разделяемой памяти (файл -shm), а разделяемая память между хостами не работает — по этой же причине WAL не работает на сетевой файловой системе4. Внутри -shm держится индекс страниц лога, чтобы читателю не приходилось сканировать весь WAL; сам wal-index редко превышает 32 КиБ, никогда не синхронизируется и удаляется при закрытии последнего соединения4.

2.1. Чекпойнт и размер лога

WAL не бесконечен только потому, что его содержимое периодически переписывается в основной файл. По умолчанию чекпойнт запускается автоматически, когда лог достигает 1000 страниц, то есть примерно 4 МБ при стандартном размере страницы4. Чекпойнт не обрезает файл — он начинает перезаписывать его с начала, если только не задан journal_size_limit4.

Ключевое ограничение: чекпойнт может работать параллельно с читателями, но обязан остановиться на странице, которая находится дальше end mark любого активного читателя, иначе он перезапишет данные, которые читатель ещё использует4. Позиция запоминается, следующий вызов продолжит с того же места4. Отсюда — неочевидное следствие: долгая читающая транзакция не просто «держит снимок», она останавливает переработку лога.

2.2. Долговечность: что именно теряется

PRAGMA synchronous=NORMAL в WAL — самый распространённый компромисс, и документация формулирует его точно: база остаётся консистентной при потере питания, но транзакция, закоммиченная в этом режиме, может откатиться после сбоя питания или падения системы; при падении только приложения транзакции сохраняются при любой настройке5. При synchronous=FULL в WAL каждый коммит синхронизирует лог, и база становится полностью ACID5.

Это же свойство объясняет, почему в WAL с NORMAL чекпойнт — единственная операция, которая делает fsync: если вынести чекпойнтинг в отдельный поток, основной поток никогда не блокируется на синхронизации, но теряет долговечность4. Для self-hosted сервиса с ежедневным бэкапом это обычно приемлемо; для базы заказов — нет, и решение принимается не «на глаз», а из требования RPO.

3. Одна запись за раз: как это выглядит в коде

В режиме rollback конкурентность устроена через состояния блокировок: SHARED для чтения, RESERVED для намерения писать, PENDING как шлагбаум для новых читателей, EXCLUSIVE для записи в файл3. PENDING существует ровно для того, чтобы писатель не голодал под потоком читателей: новые SHARED-локи не выдаются, а существующие дочитывают3. WAL эту машинерию заменяет: писатель просто дописывает в лог, поэтому писатель всегда один, а читателей — сколько угодно4.

3.1. DEFERRED, IMMEDIATE и цена апгрейда

Транзакция по умолчанию — DEFERRED: BEGIN не берёт блокировок, первая инструкция SELECT открывает читающую транзакцию, а первая запись пытается превратить её в пишущую6. Именно этот апгрейд и ломается под нагрузкой. Документация говорит прямо: если другое соединение уже изменило базу или уже изменяет её, апгрейд невозможен, и инструкция завершится с SQLITE_BUSY6.

В WAL у этой ситуации есть отдельный расширенный код. SQLITE_BUSY_SNAPSHOT (517) возвращается, когда соединение пытается превратить читающую транзакцию в пишущую и обнаруживает, что другой процесс уже записал данные, сделав прочитанный снимок устаревшим7. Сценарий из документации: процесс A открыл читающую транзакцию и держит её, процесс B обновил строки, которые A уже прочитал, после чего A пытается писать — и получает ошибку, а не ожидание7.

BEGIN IMMEDIATE, наоборот, берёт писательскую блокировку сразу и может вернуть SQLITE_BUSY только если пишущая транзакция уже активна6. Разница между двумя формами — это разница между «подождать в очереди» и «сделать половину работы и узнать, что её надо выбросить».

3.2. busy_timeout и что он не лечит

По умолчанию busy handler не установлен вовсе: если callback равен NULL, SQLITE_BUSY возвращается немедленно8. PRAGMA busy_timeout=N ставит обработчик, который спит и повторяет попытку, пока не истечёт N миллисекунд; значение меньше или равное нулю отключает ожидание9.

Здесь есть тонкость, которую пропускают: наличие busy handler не гарантирует, что он будет вызван. Если SQLite определяет, что ожидание приведёт к дедлоку — например, один процесс держит читающую блокировку и хочет стать писателем, а второй уже держит писательскую, — он сразу возвращает SQLITE_BUSY, чтобы первый отпустил свой лок8. Поэтому «поставил timeout 30 секунд» не превращает SQLite в очередь; в дедлок-сценарии ожидания не будет вообще.

Языки и фреймворки задают эти параметры по-разному, и различия объясняют половину жалоб на «database is locked». В Python sqlite3.connect() по умолчанию имеет timeout=5.0 и isolation_level='DEFERRED'10. В Rails адаптер SQLite3 объявляет :timeout как «время ожидания блокировки в миллисекундах», и по умолчанию — «no wait», то есть без ожидания11. Django с версии 5.1 добавил опцию transaction_mode и прямо рекомендует IMMEDIATE, чтобы транзакции действительно ждали до истечения timeout, а не падали с OperationalError: database is locked12. Формулировка в документации Django при этом осталась прежней: такие ошибки означают, что приложение требует от SQLite больше конкурентности, чем тот выдерживает в конфигурации по умолчанию, и увеличение таймаута «не решает проблему, а откладывает её»12.

Отдельно стоит знать про SQLITE_BUSY_TIMEOUT (773): этот код возникает, когда блокирующий запрос POSIX-блокировки на уровне VFS не уложился в таймаут, и доступен он только при сборке с SQLITE_ENABLE_SETLK_TIMEOUT — то есть при использовании проприетарного расширения блокирующих локов вместо опроса7. В обычной сборке такого поведения нет.

3.3. Очередь записи как архитектура

Практический вывод из предыдущих разделов простой: если писателей много, их надо сериализовать в приложении, а не в движке. Один выделенный писатель, который забирает задания из очереди и пачкой выполняет их в одной транзакции, убирает конфликты «писатель против писателя» и даёт батчинг: тысяча отдельных INSERT — это тысяча коммитов и тысяча fsync, одна транзакция — один13. Это не теория: независимый бенчмарк на смешанной нагрузке 80/20 показывает, что p99 чтения остаётся ниже 6 мс, а p99 записи — ниже 10 мс, пока число воркеров не превышает число аппаратных потоков, после чего p99 записи растёт логарифмически13.

Ограничение этой схемы — «горячая страница». В B-дереве SQLite каждая таблица и каждый индекс лежат отдельными деревьями, и две транзакции конфликтуют, только если пишут в близкие ключи; монотонно растущий INTEGER PRIMARY KEY при этом «почти гарантированно» кладёт все новые строки на одну страницу14. Поэтому даже оптимистичный режим с несколькими писателями не спасает приложение, которое пишет события с автоинкрементным ключом.

4. Чекпойнт — это операция, а не деталь

У чекпойнта четыре режима, и выбор между ними — это выбор между «не мешать» и «гарантированно завершить». PASSIVE делает сколько успеет, не дожидаясь читателей и писателей, и busy handler в этом режиме не вызывается вовсе15. FULL ждёт, пока не останется писателя и все читатели не перейдут на самый свежий снимок, после чего переписывает весь лог и синхронизирует файл; новые писатели блокируются, читатели — нет15. RESTART делает то же и дополнительно ждёт, пока все читатели перестанут пользоваться логом, чтобы следующий писатель начал с начала15. TRUNCATE добавляет обрезку файла до нуля15.

Все вызовы берут эксклюзивную «чекпойнт-блокировку» на файл базы: если другой процесс уже выполняет чекпойнт, вернётся SQLITE_BUSY, и busy handler не поможет15. А FULL, RESTART и TRUNCATE берут ещё и писательскую блокировку, то есть под нагрузкой могут стоять в очереди за писателем15. Отсюда практическое правило: автоматический чекпойнт (всегда PASSIVE) — фон, а TRUNCATE — операция обслуживания на ночь, а не «на всякий случай при каждом коммите».

4.1. Голодание чекпойнта

Документация перечисляет способы получить неограниченно растущий WAL4. Первый — отключить автоматический чекпойнт и забыть про ручной. Второй — голодание: если у базы много перекрывающихся читателей и хотя бы один активен всегда, ни один чекпойнт не сможет завершиться, и лог будет расти без предела4. Третий — очень большие пишущие транзакции: пока транзакция не закончена, лог не сбросить, и он может вырасти до размера самой транзакции4.

Лечение в документации названо прямо: нужны «reader gaps» — промежутки, когда базу никто не читает, и чекпойнты в эти промежутки; либо ручные RESTART/TRUNCATE, у которых есть цена — читатели могут блокироваться на время чекпойнта4. Наблюдаемость здесь встроенная: PRAGMA wal_checkpoint(NOOP) не переписывает ни одной страницы, но возвращает число фреймов в логе и число уже переписанных — этого достаточно, чтобы увидеть голодание до того, как лог займёт диск15.

Сторонние замеры добавляют практическую деталь: увеличение wal_autocheckpoint с 1000 до 4000 страниц снижает частоту «замираний» на записи, но делает сам чекпойнт дороже и удлиняет окно, в котором лог большой13. Это тот случай, где нет универсального значения: параметр выводится из того, что для сервиса важнее — p99 записи или скорость чтения.

5. Пределы одного файла

Формальные пределы SQLite огромны и задокументированы точно. Максимальное число страниц в файле — 4 294 967 294; при размере страницы 4096 байт это около 17,5 ТБ, при максимальных 65 536 байтах — около 281 ТБ16. Максимальный размер строки или BLOB — 1 млрд байт, максимальное число колонок — 2000, таблиц в join — 64, присоединённых баз — 10 по умолчанию и не больше 12516. Ограничение на число строк теоретически 2^64, но до него не добраться: раньше кончится место16.

Важнее формальных пределов два неочевидных. Первое: схема целиком читается и парсится при каждом открытии базы, поэтому время открытия и стартовый объём памяти пропорциональны размеру схемы16. Для базы с тысячами таблиц это означает, что «просто открыть соединение» может быть дороже самого запроса; в 3.54.0 появился SQLITE_LIMIT_SCHEMA с дефолтом в 10 млн объектов, чтобы этот расход можно было ограничить16.

Второе: файл — это один узел. Все 281 ТБ доступны только локально, а не по сети, потому что иначе теряются гарантии блокировок17. Предел, который действительно наступит раньше, — не объём, а время восстановления: пока файл копируется, сервис либо лежит, либо работает на устаревших данных, и никакая настройка SQLite этого не меняет.

6. Файл — не сервис: сеть, fork и close

Документация по работе через сеть начинается с предупреждения, а не с инструкции: открыть файл базы по сетевому пути можно, иногда это «работает», но полагаться на ранний успех тестирования нельзя17. Механика отказа описана там же: при доступе к локальному диску завершение записи и fsync означают, что данные либо уже сохранены, либо будут сохранены раньше последующих; в сети пакеты могут приходить не по порядку и теряться, а sync и блокировки реализованы по-разному в разных ФС17. Рекомендация — три варианта: клиент-серверная СУБД, SQLite в WAL с прокси на той же машине или SQLite в режиме rollback с одним писателем17.

Документация о способах испортить базу добавляет список, который полезно знать целиком, потому что каждый пункт встречается в реальной эксплуатации18:

  • Блокировки NFS. SQLite полагается на POSIX advisory locks, а они «известны как багованные или вовсе не реализованные» во многих реализациях NFS; при двух и более процессах это прямой путь к порче18.
  • close() в чужом потоке. Любой поток процесса, закрывший файловый дескриптор на файл базы, снимает POSIX-локи у всех остальных потоков — и те продолжают работать, считая, что держат блокировку18. С версии 3.51.0 (2025-11-04) в SQLite добавлены дополнительные защиты от этого сценария в WAL при доступе из нескольких процессов, но авторы прямо пишут, что это не панацея18.
  • Две копии SQLite в одном процессе. Каждая ведёт свой список открытых файлов, поэтому защиты друг о друге не знают; известен коммерческий продукт, который так и выпустили18.
  • fork() с открытым соединением. Соединение, унаследованное дочерним процессом, использовать нельзя; нельзя даже вызывать sqlite3_close() на нём из ребёнка18.
  • Удаление или переименование файла на ходу. Два процесса начинают работать с разными файлами под одним именем и общим -wal18.
  • Удаление -wal или -journal. Файл лога — часть состояния базы; единственный безопасный способ убрать его — открыть и закрыть базу штатно4.

Практический вывод из этого раздела один: SQLite безопасен ровно в той степени, в какой вы не трогаете его файлы руками и не ставите их на сеть. Любая «оптимизация» через symlink, bind-mount из контейнера в контейнер или общий том на NFS ломает именно те гарантии, ради которых база и выбрана.

7. Бэкап и репликация

Копирование файла «на горячую» — самый распространённый способ получить испорченный бэкап: копия может содержать часть старой и часть новой транзакции18. Документация перечисляет три безопасных способа: VACUUM INTO, C-интерфейс backup API и утилиту sqlite3_rsync, появившуюся в 3.47.0 (2024-10-21) и умеющую копировать живую базу по SSH с экономией трафика18. У sqlite3_rsync были ограничения — обе базы должны быть в WAL и с одинаковым размером страницы; в 3.50.0 (2025-05-29) их сняли, а реплика остаётся доступной только для чтения, пока идёт синхронизация19.

7.1. Litestream: забрать чекпойнтинг себе

Litestream не читает базу запросами — он перехватывает чекпойнтинг. Процесс открывает долгую читающую транзакцию, чтобы никто другой не смог чекпойнтить и перезапустить WAL, затем сам читает новые страницы лога и сам вызывает чекпойнты, упаковывая страницы в файлы LTX с монотонно растущим TXID и контрольными суммами20. Синхронизация режется по max-sync-wal-bytes (по умолчанию 64 МиБ), причём граница проверяется только после commit-маркера, чтобы не разрезать транзакцию20. Дальше работает компакция: уровень L1 собирается из 30-секундного окна, L2 — из 5-минутного, L3 — из часового, полный снимок делается раз в сутки20.

Важное свойство, которое часто понимают неправильно: гранулярность восстановления определяется границами файлов, а не частотой записи. Восстановление применяет LTX-файлы целиком и никогда частично, поэтому точка восстановления — это граница файла; после истечения l0-retention (по умолчанию 5 минут) самые тонкие точки исчезают навсегда, и доступными остаются только границы L120.

В версии 0.5.0 авторы отказались от «поколений» и теневых WAL-файлов: при разрыве непрерывности лога Litestream просто делает новый снимок с очередным TXID, а на базу теперь допускается ровно одно хранилище-приёмник21. Логика «одно место истины» честная, но она означает, что мультирегиональная репликация — не задача Litestream.

7.2. LiteFS: FUSE и конец поддержки

LiteFS шёл дальше: он поднимает FUSE-файловую систему, перехватывает вызовы, которыми SQLite пишет журнал или WAL, и складывает наборы страниц в те же LTX-файлы, отслеживая позицию репликации как пару «TXID + скользящая контрольная сумма», а первичный узел выбирается через лизинг в Consul22. Идея — дать SQLite ту же схему primary/read-replica, что у PostgreSQL: чтения локально, записи предсказуемо.

Однако в документации Fly.io сейчас стоит предупреждение: LiteFS вне области поддержки, «мы не можем предоставлять поддержку или рекомендации по нему, используйте с осторожностью»22. Автор Litestream в анонсе 0.5.0 объясняет это прямо: «рынок проголосовал», пользователи предпочли Litestream, потому что его проще запускать и понимать, и фокус вернули на него21. Для выбора инструмента это значит, что LiteFS сегодня — не опция по умолчанию для нового развёртывания.

7.3. D1: сессии, bookmarks и sequential consistency

Cloudflare D1 решает ту же задачу иначе: один первичный экземпляр, асинхронные read-реплики в шести регионах и Sessions API, который привязывает набор запросов к «сессии» и гарантирует последовательную консистентность через bookmarks23. Механика прозрачна: запись обрабатывается первичным узлом и возвращает bookmark 100; следующий запрос чтения уходит на реплику вместе с этим bookmark, и реплика ждёт, пока не догонит хотя бы до 100, прежде чем ответить23.

Это не «SQLite по сети», а другая система с другой моделью: реплики асинхронны и «в любой момент могут отставать произвольно», а последовательная консистентность обеспечивается только внутри сессии — запросы, выполненные вне объекта сессии, не гарантированно согласованы с ней23. В блоге Cloudflare мотивация объяснена без прикрас: при «плавающей» маршрутизации между репликами единственная гарантия без сессий — что прочитанное было когда-то закоммичено, то есть read committed, чего для бизнес-логики обычно недостаточно24. Цена — bookmarks надо протаскивать через запросы и ответы, а запись всё равно упирается в один первичный узел.

8. Что изменилось в 2026

Для темы «SQLite в продакшене» этот год дал два практически значимых события.

Первое — ошибка, которую в документации называют «WAL-reset bug». 3 марта 2026 года разработчик SQLite нашёл и исправил дефект, способный в редких случаях приводить к порче базы; он присутствовал во всех версиях с 3.7.0 (2010) по 3.51.2 (2026-01-09), исправлен в 3.51.3 (2026-03-13), бэкпорты есть для 3.44.6 и 3.50.74. Условия: база в WAL, два и более соединения в разных потоках или процессах, и они пытаются писать или чекпойнтить в один и тот же момент; разработчики не смогли воспроизвести дефект органически и добавили в тесты специальную логику, чтобы вызвать гонку намеренно4. Механика — гонка между чекпойнтом и коммитом, который перезапускает лог: второй чекпойнт не замечает сброса и оставляет в заголовке wal-index неверное поле, из-за чего третий чекпойнт пропускает часть транзакции4.

Второе — обычный релизный прогресс. В 3.54.0 (2026-10-09) авторы заявляют ускорение примерно на 4% по speedtest1 и на 7,5% по сравнению с 3.51.0, добавили лимит на число объектов схемы и переработали CLI25. Через неделю, 16 октября, вышел 3.54.1 с исправлением возможного переполнения буфера в куче на 32-битных сборках (64-битные не затронуты) и другими правками по сообщениям пользователей26.

Практический вывод из обоих пунктов одинаковый: версия SQLite — часть конфигурации продакшена, а не деталь, которую выбирает дистрибутив. Проверять sqlite3.sqlite_version в работающем приложении дешевле, чем выяснять, почему база «иногда» не сходится с бэкапом.

9. Дискуссионные вопросы и противоречия

«Одна запись — не проблема». Документация утверждает, что писатели просто встают в очередь, а транзакция занимает миллисекунды, поэтому SQLite выдерживает больше конкурентности, чем принято думать2. Бенчмарки это подтверждают для низкой конкурентности и опровергают для высокой: на смешанной нагрузке p99 записи остаётся ниже 10 мс только до числа воркеров, равного числу аппаратных потоков, дальше растёт логарифмически13. Противоречия здесь нет — есть разные определения «проблемы»: документация говорит о среднем времени, бенчмарк — о хвосте распределения, а именно хвост виден пользователю.

«WAL ускоряет всё». Независимый замер даёт неожиданную кривую: при одном и двух одновременных соединениях WAL снижает пропускную способность записи на 43% и 17% соответственно, и только выше этого начинает выигрывать, а p99 записи улучшается на 30–60% начиная с двух с лишним писателей13. Это согласуется с документацией, где сказано, что на преимущественно читающей нагрузке WAL может быть на 1–2% медленнее4. Вывод: WAL — не «включить и забыть», а решение под конкретный профиль нагрузки.

«BEGIN CONCURRENT решает проблему одного писателя». В ветке begin-concurrent несколько пишущих транзакций действительно идут одновременно: блокировка берётся только на COMMIT, а конфликты выявляются оптимистичной постраничной проверкой, после чего транзакция получает SQLITE_BUSY_SNAPSHOT и может только откатиться14. Но и здесь есть два ограничения: коммиты всё равно сериализуются, а конфликты возникают тем чаще, чем ближе ключи, — то есть типичная запись событий с автоинкрементом остаётся худшим случаем, и документация прямо советует такие ключи делать случайными14. В обычных релизах SQLite этого режима нет: он живёт в отдельном дереве hctree14, и в списке изменений SQLite за 2026 год он не упоминается26.

«Репликация = высокая доступность». Все три инструмента дают разную гарантию. Litestream — асинхронная потоковая репликация в объектное хранилище, то есть бэкап с малой задержкой, а не отказоустойчивость: при падении сервера данные после последней синхронизации теряются, а восстановление занимает время на сборку из LTX20. LiteFS был ближе к HA, но потерял поддержку22. D1 даёт управляемую HA, но требует правки кода под сессии и переносит запись в один регион23. Общего «просто добавь репликацию» для SQLite не существует.

«D1 — это SQLite». Формат файла и SQL совпадают, но модель консистентности — нет: асинхронные реплики, bookmark-и, последовательная консистентность в пределах сессии и «read committed» вне её23. Приложение, написанное под локальный файл, при переносе в D1 получает не «ту же базу по сети», а распределённую систему с другими правилами.

10. Недостаточность данных

Три вопроса закрыть не удалось. Первый: нет публичных измерений доли SQLITE_BUSY в реальных нагрузках — все доступные цифры либо синтетические, либо вендорские. Второй: влияние остановки чекпойнта на хвостовую задержку описано качественно, но воспроизводимых замеров «сколько мс стоит один PASSIVE-чекпойнт на базе размера N» в источниках нет. Третий: статус и планы ветки begin-concurrent не формализованы — документ описывает поведение, но не говорит, когда и войдёт ли режим в основную линию; в release notes за 2026 год он не появляется2614. Отдельно стоит упомянуть, что частота проявления WAL-reset bug в природе неизвестна: разработчики не смогли воспроизвести его без специальной тестовой логики4.

11. Практический чеклист

Для сервиса на SQLite, который вы собираетесь держать в продакшене, порядок действий выглядит так.

  1. Включить WAL и осознанно выбрать долговечность. PRAGMA journal_mode=WAL; — режим сохраняется в файле и применяется ко всем соединениям4. synchronous=NORMAL даёт консистентность без долговечности на сбое питания; FULL — полный ACID с fsync на каждом коммите5. Решение фиксируется в конфигурации, а не в голове разработчика.
  2. Задать таймаут и форму транзакции. busy_timeout в диапазоне 5–10 секунд13, BEGIN IMMEDIATE для любой транзакции, которая будет писать612, батчинг вставок в одну транзакцию13.
  3. Сериализовать запись в приложении. Один писатель и очередь снимают конфликты «писатель против писателя» лучше, чем любой таймаут, и позволяют вернуть 503 при переполнении очереди вместо таймаута в драйвере13.
  4. Следить за размером WAL и прогрессом чекпойнта. PRAGMA wal_checkpoint(NOOP) даёт число фреймов и число переписанных страниц15; рост первого при стоящем втором — это голодание4.
  5. Не ставить базу на сеть и не трогать файлы. Локальный диск, никаких symlink и mv, никакого общего тома между контейнерами, никакого удаления -wal184. В контейнере — том, смонтированный с локального диска хоста.
  6. Не наследовать соединение через fork() и не линковать две копии SQLite в один процесс18.
  7. Бэкапить только безопасным способом: VACUUM INTO, backup API или sqlite3_rsync, поверх — Litestream, если нужна точка восстановления в минутах, а не в сутках1820.
  8. Держать версию в актуальном состоянии и знать, что 3.51.2 и ниже содержат WAL-reset bug426.

Заключение

SQLite в продакшене ограничен не пропускной способностью, а тремя вещами, каждая из которых управляема. Чекпойнт требует пауз без читателей — иначе лог растёт, а чтение замедляется. Транзакция требует явной формы — DEFERRED под конкурентностью превращает конфликт в ошибку вместо ожидания. Файл требует локального диска и дисциплины вокруг него — сеть, symlink и чужой close() ломают гарантии, на которых держится вся модель.

Там, где эти три условия выполнены, SQLite даёт то, чего нет у клиент-серверных СУБД: нулевую сетевую задержку, отсутствие администрирования и бэкап, который можно проверить локально. Там, где они не выполнены, никакая настройка pragma не поможет — и это, пожалуй, главный критерий выбора: SQLite подходит приложению, у которого есть один узел и понятный владелец, и не подходит системе, которой нужна геораспределённая запись.

Quality Metrics

MetricValue
Total sources26
Academic sources1
Official/documentation22
Industry reports0
News/journalism0
Blog/forum3
Citation coverage83%
Counter-arguments searchedYes
Research rounds3
Questions emerged5
Questions resolved2
Questions insufficient data3

  1. Kevin P. Gaffney, Martin Prammer, Larry Brasfield, D. Richard Hipp, Dan Kennedy, Jignesh M. Patel. “SQLite: Past, Present, and Future.” PVLDB 15(12): 3535–3547, 2022. https://www.vldb.org/pvldb/vol15/p3535-gaffney.pdf ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. SQLite. “Appropriate Uses For SQLite.” sqlite.org. https://sqlite.org/whentouse.html ↩︎ ↩︎ ↩︎ ↩︎

  3. SQLite. “File Locking And Concurrency In SQLite Version 3.” sqlite.org. https://sqlite.org/lockingv3.html ↩︎ ↩︎ ↩︎

  4. SQLite. “Write-Ahead Logging.” sqlite.org. https://sqlite.org/wal.html ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  5. SQLite. “PRAGMA Statements.” sqlite.org. https://sqlite.org/pragma.html ↩︎ ↩︎ ↩︎

  6. SQLite. “Transactions.” sqlite.org. https://sqlite.org/lang_transaction.html ↩︎ ↩︎ ↩︎ ↩︎

  7. SQLite. “Result and Error Codes.” sqlite.org. https://sqlite.org/rescode.html ↩︎ ↩︎ ↩︎

  8. SQLite. “Register A Callback To Handle SQLITE_BUSY Errors.” sqlite.org. https://sqlite.org/c3ref/busy_handler.html ↩︎ ↩︎

  9. SQLite. “Set A Busy Timeout.” sqlite.org. https://sqlite.org/c3ref/busy_timeout.html ↩︎

  10. Python Software Foundation. “sqlite3 — DB-API 2.0 interface for SQLite databases.” Python 3 documentation. https://docs.python.org/3/library/sqlite3.html ↩︎

  11. Rails. “Active Record SQLite3 Adapter.” api.rubyonrails.org. https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SQLite3Adapter.html ↩︎

  12. Django Software Foundation. “Databases — SQLite notes.” Django 5.2 documentation. https://docs.djangoproject.com/en/5.2/ref/databases/ ↩︎ ↩︎ ↩︎

  13. Shivek Khurana. “SQLite in Production — A Real-World Benchmark.” 30 Dec 2025. https://shivekkhurana.com/blog/sqlite-in-production/ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  14. SQLite. “Begin Concurrent.” hctree documentation. https://sqlite.org/hctree/doc/begin-concurrent/doc/begin_concurrent.md ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  15. SQLite. “Checkpoint a database.” sqlite.org. https://sqlite.org/c3ref/wal_checkpoint_v2.html ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  16. SQLite. “Implementation Limits For SQLite.” sqlite.org. https://sqlite.org/limits.html ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  17. SQLite. “SQLite Over a Network, Caveats and Considerations.” sqlite.org. https://sqlite.org/useovernet.html ↩︎ ↩︎ ↩︎ ↩︎

  18. SQLite. “How To Corrupt An SQLite Database File.” sqlite.org. https://sqlite.org/howtocorrupt.html ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  19. SQLite. “sqlite3_rsync.” sqlite.org. https://sqlite.org/rsync.html ↩︎

  20. Litestream. “How it works.” litestream.io. https://litestream.io/how-it-works/ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  21. Ben Johnson. “Litestream v0.5.0 is Here.” Fly.io Blog, 3 Oct 2025. https://fly.io/blog/litestream-v050-is-here/ ↩︎ ↩︎

  22. Fly.io. “How LiteFS Works.” Fly Docs. https://fly.io/docs/litefs/how-it-works/ ↩︎ ↩︎ ↩︎

  23. Cloudflare. “Global read replication.” D1 documentation. https://developers.cloudflare.com/d1/best-practices/read-replication/ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  24. Cloudflare. “Sequential consistency without borders: how D1 implements global read replication.” Cloudflare Blog. https://blog.cloudflare.com/d1-read-replication-beta/ ↩︎

  25. SQLite. “SQLite Release 3.54.0 On 2026-10-09.” sqlite.org. https://sqlite.org/releaselog/3_54_0.html ↩︎

  26. SQLite. “Release History Of SQLite.” sqlite.org, 16 Oct 2026. https://sqlite.org/changes.html ↩︎ ↩︎ ↩︎ ↩︎