SQLite / Cloudflare D1 Compatibility#
Two distinct, unresolved compatibility issues affect Better Auth when running on Cloudflare Workers with a D1 (or any SQLite) database. Both were first reported against v1.6.20–v1.6.23 and remain open as of July 2026.
1. Lazily-Cached Init Promises Hang Forever on Cloudflare Workers#
Severity: High — causes a full auth outage for users routed to an affected isolate until it is evicted.
Root Cause#
Better Auth resolves AsyncLocalStorage through a module-level dynamic import :
const AsyncLocalStoragePromise = import("node:async_hooks").then(...)
On Cloudflare Workers (nodejs_compat), a promise created inside a request's IoContext is owned by that request. When the client disconnects (e.g., via AbortController.abort()), workerd never settles the promise — it simply stops. Because AsyncLocalStoragePromise and the auth instance's $context are cached at module scope, every subsequent request in that isolate awaits a permanently-pending promise and hangs indefinitely (no error, no timeout).
There is a related but separate concurrency race in ensureAsyncStorage (issue #10300) where concurrent cold-start requests can overwrite a live ALS instance; PR #9862 proposes a fix for that race but explicitly does not address the abort-poisoning scenario.
Symptoms#
- A small percentage of
auth.api.getSession()calls hang forever with near-zero CPU. - Raw D1 queries (
env.DB.prepare(...)) in the same isolate succeed immediately — the hang is upstream of any DB call, in the auth dispatch layer. - Hung requests arrive in bursts (isolate-scoped), with
clientDisconnectedoutcomes in Cloudflare analytics and zero error-count in the dashboard. - Affected versions: v1.6.20, v1.6.22, v1.6.23 (multiple production reports).
Workarounds (verified in production)#
-
Pre-seed AsyncLocalStorage instances at module scope using a static (not dynamic)
node:async_hooksimport so the lazy import path is never taken:import { AsyncLocalStorage } from "node:async_hooks"; // Seed the three storages before any request arrives: // requestStateAsyncStorage, endpointContextAsyncStorage, adapterAsyncStorage // on globalThis[Symbol.for("better-auth:global")].context -
Anchor
auth.$contexttoctx.waitUntilonce per isolate, so initialization is driven by the runtime rather than a client request:export default { async fetch(request, env, ctx) { ctx.waitUntil(auth.$context); // anchor init return auth.handler(request); } } -
Add a hang-detector timeout that rebuilds the auth instance if
auth.handler()does not respond within a threshold:const result = await Promise.race([ auth.handler(request), new Promise((_, reject) => setTimeout(() => reject(new Error("hang")), 5000)) ]);
The underlying fix would be to check for globalThis.AsyncLocalStorage before falling through to the dynamic import, or to create ALS instances synchronously when the global is available.
Key files:
packages/core/src/async_hooks/index.ts—AsyncLocalStoragePromisedefinitionpackages/core/src/context/request-state.ts—ensureAsyncStoragememoization logic- Issue #10315 — primary report with full diagnostics and production telemetry
- Issue #10300 / PR #9862 — related concurrency race (unmerged fix)
2. Spurious "Expected number but got bigint" Warning on SQLite/D1#
Severity: Low — warning-only, no functional breakage.
Root Cause#
When rateLimit.storage: "database" is enabled, Better Auth creates the rateLimit.lastRequest column as bigint . However, the matchType() validator's SQLite map does not include "BIGINT" in the accepted types for number fields :
// Current (incomplete):
sqlite: { number: ["INTEGER", "REAL"], ... }
// MySQL/MSSQL (correct precedent):
mysql: { number: ["integer", "int", "bigint", ...], ... }
This causes getMigrations to warn on every run after the table is first created. The warning is spurious: the rate limiter already coerces bigint → Number at runtime, so throttling works correctly.
Fix#
Add "BIGINT" to map.sqlite.number in packages/better-auth/src/db/get-migration.ts, mirroring the MySQL/MSSQL maps. SQLite stores BIGINT-declared columns with INTEGER affinity (64-bit), so this is schema-safe. No PR has been filed yet — this is a one-line change suitable for a community contribution.
Key files:
packages/better-auth/src/db/get-migration.ts—matchType()andgetType()functions- Issue #10306 — root cause analysis and suggested fix