Skip to content

adapter-cloudflare 8: concurrent Vite servers each start a platform proxy, so Vitest projects collide on the same SQLite (SQLITE_BUSY_RECOVERY) #17344

Description

@longrunningprocess

Describe the bug

virtual_workers_module()'s setup() guards on globalThis.__sveltekit_cloudflare_platform and only assigns it after await getPlatformProxy(options) has finished:

const setup = async () => {
	if (globalThis.__sveltekit_cloudflare_platform) return;
	const proxy = await getPlatformProxy(options);
	globalThis.__sveltekit_cloudflare_platform = proxy;
	...
};

When two Vite servers start in the same process, both pass the guard before either finishes, so each starts its own getPlatformProxy() (its own workerd) on the same .wrangler/state. This is the same shape as #16623 (which described the old emulate() path), but in the new Vite plugin. Vitest triggers it every time: each project that extends a config using sveltekit({ adapter: adapter() }) gets its own Vite server, so a two-project setup (say client and server) starts two proxies at once.

The result is intermittent. Most often one workerd is repairing a journal left by an earlier unclean shutdown (a killed vite dev, say) while the other opens the same SQLite files, and the run dies in about two seconds:

Using secrets defined in .dev.vars
Using secrets defined in .dev.vars
*** Fatal uncaught kj::Exception: workerd/util/sqlite.c++:890: failed: SQLite failed; dbErrorMessage(prepareResult, db) = database is locked: SQLITE_BUSY (extended: SQLITE_BUSY_RECOVERY)

⎯⎯⎯⎯⎯⎯⎯ Startup Error ⎯⎯⎯⎯⎯⎯⎯⎯
AggregateError: Failed to initialize projects. There were errors during projects setup.
  MiniflareCoreError [ERR_RUNTIME_FAILURE]: The Workers runtime failed to start.
  ... at getPlatformProxy

When it doesn't fail, the run still ends with Tests closed successfully but something prevents the main process from exiting, even with the dispose fix from #17238.

Reproduction

  1. A SvelteKit 3 project with @sveltejs/adapter-cloudflare 8.0.0 and a wrangler.jsonc with a D1 binding.

  2. A vite.config.ts with sveltekit({ adapter: adapter() }) and two Vitest projects that extends it:

    test: { projects: [
    	{ extends: './vite.config.ts', test: { name: 'client', /* browser */ } },
    	{ extends: './vite.config.ts', test: { name: 'server', environment: 'node' } },
    ] }
  3. vitest --run repeatedly. The setup prints "Using secrets defined in .dev.vars" twice (or two proxies in whatever form your logs show).

In my project, 3 of 5 consecutive runs failed this way (about 2 s each); the other two passed in about 19 s with the exit warning above. Failures are most likely right after vite dev, wrangler dev or a build, which leave state behind.

Suggested fix

Store the pending promise rather than the finished proxy, so concurrent servers share one proxy. I patched my local copy of 8.0.0 this way:

const setup = async () => {
	if (globalThis.__sveltekit_cloudflare_platform) return;
	const pending = (globalThis.__sveltekit_cloudflare_platform_pending ??= getPlatformProxy(options));
	const proxy = await pending;
	if (globalThis.__sveltekit_cloudflare_platform) return;
	globalThis.__sveltekit_cloudflare_platform = proxy;
	globalThis.caches = proxy.caches;
};

(dispose would also need to clear the pending promise.) With it, 5 of 5 runs started a single proxy, none failed, each took about 7 s instead of 19 s, and the exit warning was gone. I haven't checked the preview server or a restarting dev server.

Workaround for now: leave the adapter out of the Vite config when process.env.VITEST is set. Unit tests that don't need bindings don't need the proxy at all.

System Info

@sveltejs/kit 3.0.0, @sveltejs/adapter-cloudflare 8.0.0, wrangler 4.147.0
vite 8.3.2, vitest 4.1.11, @vitest/browser-playwright 4.1.11
node 26.10.0, bun 1.4.2, macOS 27.0.1

Severity

annoyance (intermittent test-start failure with a workaround)

Activity

  1. glw907 commented on Oct 6, 2026

    @glw907

    The same global also fails in the opposite direction: a second Vite server that never created the proxy still disposes it when it closes.

    In virtual_workers_module() (packages/adapter-cloudflare/index.js at 2ec690e), setup() returns early when globalThis.__sveltekit_cloudflare_platform is already set (line 226). The dispose that #17238 added (lines 233-237) runs from closeServer for every server (lines 242-245), whether or not that server's setup created the proxy. A plugin that starts and closes a short-lived second server during vite dev therefore disposes the proxy the main dev server still uses, and clears the global. A route that reads cloudflare:workers then fails.

    To reproduce, with @sveltejs/kit 3.0.1, @sveltejs/adapter-cloudflare 8.0.0, Vite 8.3.3, and wrangler 4.147.0:

    1. Scaffold an app with sv create app --template minimal --types jsdoc --no-add-ons, and pass adapter: adapter() to sveltekit() in vite.config.js.

    2. Add a wrangler.jsonc with main and assets set as the adapter docs describe, and "vars": { "FOO": "bar" }.

    3. Add src/routes/+page.server.js:

      import { env } from 'cloudflare:workers';
      export const load = () => ({ foo: env.FOO });
    4. Add this plugin after sveltekit():

      {
      	name: 'nested-server',
      	apply: 'serve',
      	async buildStart() {
      		if (process.env.NESTED) return;
      		process.env.NESTED = '1';
      		const { createServer } = await import('vite');
      		const server = await createServer({ server: { middlewareMode: true } });
      		await server.close();
      	}
      }
    5. Run vite dev and request /.

    The request returns a 500:

    Error: Cannot access cloudflare:workers in a prerenderable route
        at load (src/routes/+page.server.js:2:39)
    

    Without the plugin, / returns 200. With the plugin but without the server.close() call, it also returns 200, so the failure comes from the close.

    Both this case and the concurrent-start case come from the global holding one proxy with no record of which servers use it. I've opened #17368, which shares one pending proxy promise across servers, counts the servers using it, and disposes it when the last one closes.

  2. imlargo commented on Oct 7, 2026

    @imlargo

    Same failure on a clean GitHub Actions runner, so journal recovery after an unclean shutdown is not the only trigger. ubuntu-latest, pnpm install --frozen-lockfile, no .wrangler/ directory, no bindings besides ASSETS, the two Vitest projects sv generates (client in Chromium, server in Node), then vitest --run:

    ⎯⎯⎯⎯⎯⎯⎯ Startup Error ⎯⎯⎯⎯⎯⎯⎯⎯
    AggregateError: Failed to initialize projects. There were errors during projects setup. See below for more details.
        MiniflareCoreError [ERR_RUNTIME_FAILURE]: The Workers runtime failed to start. There was likely a problem with the workerd binary or your configuration.
          code: 'ERR_RUNTIME_FAILURE',
    *** Fatal uncaught kj::Exception: workerd/util/sqlite.c++:1733: failed: SQLite failed; dbErrorMessage(err, db) = database is locked: SQLITE_BUSY
    

    Plain SQLITE_BUSY here, not SQLITE_BUSY_RECOVERY. Log: https://github.com/imlargo/svelte-template/actions/runs/37266332359 (Test job). @sveltejs/kit 3.0.0, @sveltejs/adapter-cloudflare 8.0.0, vitest 4.1.11, wrangler 4.147.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions