From a74ce48f9630e3e672f687c1cb3b38f2187ecf2d Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Tue, 13 Oct 2026 17:54:43 -0700 Subject: [PATCH] fix(server): stop fsyncing SQLite on every commit so slow disks no longer drop clients (#19205) Co-authored-by: Claude Opus 5.5 --- apps/server/src/persistence/Sqlite.test.ts | 15 +++++++++++++++ apps/server/src/persistence/Sqlite.ts | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/apps/server/src/persistence/Sqlite.test.ts b/apps/server/src/persistence/Sqlite.test.ts index 666e14d3a5cc69f39b3ceae7b54609df9bfe717e..f4056c9d0c2fa918069faa10240346ec8bafa295 100644 --- a/apps/server/src/persistence/Sqlite.test.ts +++ b/apps/server/src/persistence/Sqlite.test.ts @@ -114,6 +114,21 @@ it.effect("collects planner statistics for an existing database on open", () => ); }); +it.effect("only fsyncs the database file at WAL checkpoints", () => { + const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-sqlite-sync-")); + const dbPath = NodePath.join(tempDir, "state.sqlite"); + + return Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const rows = yield* sql<{ readonly synchronous: number }>`PRAGMA synchronous`; + // 1 is NORMAL. + assert.equal(rows[0]?.synchronous, 1); + }).pipe( + Effect.provide(SqlitePersistence.layerFromPath(dbPath).pipe(Layer.provide(NodeServices.layer))), + Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))), + ); +}); + it.effect("applies busy_timeout in the shared persistence setup", () => Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; diff --git a/apps/server/src/persistence/Sqlite.ts b/apps/server/src/persistence/Sqlite.ts index 2ea5199b28337748918e1e5fd08d0bf52204994f..d6adf41f3d957a702f306daf516b679fbbf62a5c 100644 --- a/apps/server/src/persistence/Sqlite.ts +++ b/apps/server/src/persistence/Sqlite.ts @@ -19,6 +19,12 @@ const layerSetup = Layer.effectDiscard( yield* sql`PRAGMA busy_timeout = 5000;`; yield* sql`PRAGMA foreign_keys = ON;`; yield* sql`PRAGMA journal_mode = WAL;`; + // FULL fsyncs the WAL on every commit, on the main thread. Every provider + // event is its own commit, so a slow or busy disk stalls the event loop + // and clients drop. In WAL mode NORMAL only fsyncs at checkpoints. It + // cannot corrupt the database and an app crash loses nothing; an OS crash + // or power loss can roll back commits since the last checkpoint. + yield* sql`PRAGMA synchronous = NORMAL;`; // PASSIVE checkpoints never shrink the -wal file, so it otherwise keeps its // largest size until the last connection closes. yield* sql.unsafe(`PRAGMA journal_size_limit = ${WAL_SIZE_LIMIT_BYTES};`); -- 2.54.0