authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-13 17:54:43-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-13 17:54:43-07:00
loga74ce48f9630e3e672f687c1cb3b38f2187ecf2d
tree799a552cd50b0a0a7f5ef41cee138a804801e30b
parentaef0e9ca401acb40057a286a19de8768809db960
signature Signed by PGP key B5690EEEBB952194

fix(server): stop fsyncing SQLite on every commit so slow disks no longer drop clients (#19205)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

2 files changed, 21 insertions(+), 0 deletions(-)

apps/server/src/persistence/Sqlite.test.ts+15
......@@ -114,6 +114,21 @@ it.effect("collects planner statistics for an existing database on open", () =>
114114 );
115115});
116116
117it.effect("only fsyncs the database file at WAL checkpoints", () => {
118 const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-sqlite-sync-"));
119 const dbPath = NodePath.join(tempDir, "state.sqlite");
120
121 return Effect.gen(function* () {
122 const sql = yield* SqlClient.SqlClient;
123 const rows = yield* sql<{ readonly synchronous: number }>`PRAGMA synchronous`;
124 // 1 is NORMAL.
125 assert.equal(rows[0]?.synchronous, 1);
126 }).pipe(
127 Effect.provide(SqlitePersistence.layerFromPath(dbPath).pipe(Layer.provide(NodeServices.layer))),
128 Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))),
129 );
130});
131
117132it.effect("applies busy_timeout in the shared persistence setup", () =>
118133 Effect.gen(function* () {
119134 const sql = yield* SqlClient.SqlClient;
apps/server/src/persistence/Sqlite.ts+6
......@@ -19,6 +19,12 @@ const layerSetup = Layer.effectDiscard(
1919 yield* sql`PRAGMA busy_timeout = 5000;`;
2020 yield* sql`PRAGMA foreign_keys = ON;`;
2121 yield* sql`PRAGMA journal_mode = WAL;`;
22 // FULL fsyncs the WAL on every commit, on the main thread. Every provider
23 // event is its own commit, so a slow or busy disk stalls the event loop
24 // and clients drop. In WAL mode NORMAL only fsyncs at checkpoints. It
25 // cannot corrupt the database and an app crash loses nothing; an OS crash
26 // or power loss can roll back commits since the last checkpoint.
27 yield* sql`PRAGMA synchronous = NORMAL;`;
2228 // PASSIVE checkpoints never shrink the -wal file, so it otherwise keeps its
2329 // largest size until the last connection closes.
2430 yield* sql.unsafe(`PRAGMA journal_size_limit = ${WAL_SIZE_LIMIT_BYTES};`);