Skip to content

[miniflare] Share local storage across processes - #15076

Open
penalosa wants to merge 1 commit into
emily/new-config-miniflarefrom
penalosa/global-resources-new-config
Open

[miniflare] Share local storage across processes#15076
penalosa wants to merge 1 commit into
emily/new-config-miniflarefrom
penalosa/global-resources-new-config

Conversation

@penalosa

@penalosa penalosa commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Adds experimental unsafeSharedStorageOwner support so Miniflare processes using the same persistence root and resource IDs share KV, R2, D1, Images, Stream, and Secrets Store storage through a single elected owner process. Cache, Durable Objects, and Workflows remain isolated per instance to avoid storage contention.

This PR is stacked on #14994 and ports the original implementation to its v5 configuration model.


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: this is an experimental unsafe Miniflare option.

A picture of a cute animal (not mandatory, but encouraged)

Note

This is a contribution from an AI agent: OpenCode, GPT-5.6 Sol.


Open in Devin Review

@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f46c0fa

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
miniflare Minor
@cloudflare/deploy-helpers Patch
@cloudflare/pages-shared Patch
@cloudflare/remote-bindings Patch
@cloudflare/runtime-types Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-pool-workers Patch
wrangler Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ask-bonk

ask-bonk Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

No duplicate. The type import change was just reformatting.

Based on my thorough review, the implementation is well-structured, follows established patterns in the codebase (the StorageOwnerProxy mirrors ExternalServiceProxy, the per-plugin routing hooks are consistent), degrades gracefully as best-effort, and is backed by extensive tests. The remaining edge cases (owner teardown race, leaked config file on spawn failure, detached child 'error' event) are low-severity and largely inherent to the best-effort, detached-process design that the code documents openly.

I found no actionable logic bugs, security issues, backward-compatibility violations, or incorrect API behavior that warrant a change request.

LGTM

github run

@pkg-pr-new

pkg-pr-new Bot commented Aug 7, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@15076

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@15076

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@15076

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@15076

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@15076

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@15076

miniflare

npm i https://pkg.pr.new/miniflare@15076

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@15076

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@15076

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@15076

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@15076

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@15076

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@15076

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@15076

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@15076

wrangler

npm i https://pkg.pr.new/wrangler@15076

commit: f46c0fa

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +833 to +834
#storageOwnerRoutingActive = false;
readonly #storageOwnerPresenceName = `${STORAGE_OWNER_CLIENT_PRESENCE_PREFIX}${process.pid}-${storageOwnerClientCounter++}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Shared storage server shuts itself down mid-session on Windows

Each participating dev process announces itself with a name containing a colon (${STORAGE_OWNER_CLIENT_PRESENCE_PREFIX}${process.pid}-... at packages/miniflare/src/index.ts:834), which Windows cannot use as a file name, so the shared storage server never sees any participants and shuts down while they are still using it.
Impact: On Windows, shared local storage disappears a few seconds after start-up and every KV/R2/D1 read or write from the dev servers starts failing.

Presence entries are written as dev-registry files named after the entry key

DevRegistry.register() writes one file per entry at path.join(registryPath, name) (packages/miniflare/src/shared/dev-registry.ts:186-193). The presence key contains : because STORAGE_OWNER_CLIENT_PRESENCE_PREFIX ends with a colon (packages/miniflare/src/workers/core/constants.ts:110-111). On Windows dir\__miniflare_storage_owner_client__:1234-0 is an NTFS alternate data stream, so readdirSync never lists it (and reading the base name yields empty/invalid JSON, which getWorkerRegistry skips). countLiveClients() in runStorageOwnerProcess (packages/miniflare/src/index.ts:3593-3596) therefore always returns 0, and after the startup grace plus 3 idle checks the owner calls shutdown("no live clients") even though clients are actively routing storage to it.

Prompt for agents
The shared-storage client presence entries are registered in the dev registry under a name containing a colon (STORAGE_OWNER_CLIENT_PRESENCE_PREFIX in packages/miniflare/src/workers/core/constants.ts ends with ':'). DevRegistry.register writes one file per entry using the entry name as the file name, and colons are not valid in Windows file names (they create NTFS alternate data streams), so the entry becomes invisible to readdirSync/getWorkerRegistry. As a result runStorageOwnerProcess's countLiveClients always sees zero clients on Windows and the owner self-terminates while clients are live. Change the presence key to a filesystem-safe separator (e.g. a '-' or '_' instead of ':'), keeping isStorageOwnerRegistryName and the owner's counting logic in sync, and make sure the prefix cannot collide with real worker names.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +2576 to +2593
#getStorageOwnerRouting(): boolean {
const persistRoot = this.#storageOwnerPersistRoot();
if (
persistRoot === undefined ||
this.#sharedOpts.unsafeStorageOwnerRole === "owner"
) {
return false;
}

if (this.#readStorageOwnerEntry() === undefined) {
this.#log.warn(
"Shared storage owner enabled but no owner is currently registered — " +
"using local storage for this instance"
);
return false;
}
return true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Second dev process can be routed to a shared storage server that does not host its resources

The dev process that starts the shared storage server only tells it about its own resources (#spawnStorageOwner at packages/miniflare/src/index.ts:2660-2672), yet later dev processes still redirect all of their storage to it as soon as it exists, so their bindings point at storage that was never set up.
Impact: A second dev server with different resources (a Secrets Store secret, Stream, Images, or a resource type the first process didn't use) gets runtime errors on every storage access instead of working storage.

Routing decision only checks that some owner exists, not that it hosts this instance's plugins

#getStorageOwnerRouting() (packages/miniflare/src/index.ts:2576-2593) returns true purely on the presence of the owner's dev registry entry, and #assembleConfig then routes every plugin that has local resources (packages/miniflare/src/index.ts:1814-1820). The comment in #spawnStorageOwner argues the owner's entry services are generic (keyed by idFromName), which holds for KV/R2/D1 ids but not for:

  • plugins the spawning instance never used at all — e.g. the owner only gets d1:db:entry if the spawner had a local D1 binding, so a D1-only second client resolves a non-existent service;
  • Secrets Store, whose owner services are named per secret (getUserBindingServiceName(SECRET_STORE_PLUGIN_NAME, "${storeId}:${secretName}"), packages/miniflare/src/plugins/secret-store/index.ts:113-117) and whose routed binding targets that exact name (routeBindingToStorageOwner, same file);
  • Stream/Images, whose owner services only exist when the spawner declared such a binding (getStorageOwnerHosting in packages/miniflare/src/plugins/stream/index.ts:185-200 and packages/miniflare/src/plugins/images/index.ts:208-223).

In all these cases the client skips standing up its own local service (e.g. packages/miniflare/src/plugins/secret-store/index.ts:67-69), so the binding has no working target.

Prompt for agents
Clients route storage to a shared owner as soon as an owner entry exists in the dev registry, without verifying that the owner actually hosts the resources/plugins this client needs. The owner is configured once, from the spawning client's bindings only (Miniflare.#spawnStorageOwner). For KV/R2/D1 the owner's entry services are generic only if the owner had at least one binding of that type; for Secrets Store the owner services are named per store/secret; Stream/Images services exist only if the spawner declared them. A later client with different resources will route to services that don't exist on the owner and every storage op fails, while it has also skipped standing up its own local services. Consider publishing the set of hosted plugins/resources in the owner's registry entry so clients only route the plugins the owner actually hosts (falling back to local storage otherwise), or have clients hand their hosting requirements to a running owner so it can extend its configuration.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +3597 to +3611
const startedAt = Date.now();
let idleChecks = 0;
timers.idle = setInterval(() => {
if (Date.now() - startedAt < STORAGE_OWNER_STARTUP_GRACE_MS) {
return;
}
if (countLiveClients() === 0) {
idleChecks++;
if (idleChecks >= STORAGE_OWNER_IDLE_DEBOUNCE) {
void shutdown("no live clients");
}
} else {
idleChecks = 0;
}
}, STORAGE_OWNER_IDLE_CHECK_MS);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Shared storage server can exit while a dev server is still starting up

The shared storage server starts counting itself idle a short while after it becomes ready (STORAGE_OWNER_STARTUP_GRACE_MS grace plus three checks at packages/miniflare/src/index.ts:3599-3611), even though the dev server that started it only announces itself after its runtime has finished booting, so a slow boot makes the storage server quit underneath it.
Impact: On slower machines or CI, the freshly started dev server ends up pointing at storage that has already gone away, and all shared storage requests fail.

Presence is only published after workerd boots, well after the owner's grace window starts

The owner sets startedAt after await mf.ready and, once STORAGE_OWNER_STARTUP_GRACE_MS (default 10s) has elapsed, exits after STORAGE_OWNER_IDLE_DEBOUNCE (3) consecutive checks with zero clients — i.e. ~13s after readiness by default. The spawning client only registers its presence entry in #registerWorkers (packages/miniflare/src/index.ts:2804-2817), which runs at the end of #assembleAndUpdateConfig after runtime.updateConfig() has booted workerd. Between the owner registering itself (which unblocks #ensureStorageOwner) and the client's workerd being ready, more than 13s can easily pass on a cold start, so the owner tears itself down while its only client is mid-boot; the client's routed bindings then resolve to a missing owner.

Prompt for agents
The spawned storage owner begins its idle countdown STORAGE_OWNER_STARTUP_GRACE_MS after it becomes ready, but the client that spawned it only publishes its dev-registry presence entry after its own workerd finishes booting (Miniflare.#registerWorkers). If that boot takes longer than the grace + debounce window (~13s by default) the owner exits while its client is still starting, leaving the client's routed storage bindings pointing at a dead owner. Consider having the client publish its presence entry as soon as it decides to route to the owner (before workerd boots), and/or having the owner only start the idle countdown once it has observed at least one client.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +203 to +206
// Distinguishes shared-storage client presence entries when several Miniflare
// instances live in one process (tests, the Vitest pool) and would otherwise
// collide on `process.pid`.
let storageOwnerClientCounter = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 New module-level mutable counter introduced

A module-scoped mutable counter is added (let storageOwnerClientCounter = 0 at packages/miniflare/src/index.ts:206) to disambiguate instances in the same process, which the repository's review guidelines forbid.
Impact: The change conflicts with a documented repository rule and is likely to be rejected during review.

Rule reference

REVIEW.md → "Avoid using global variables. Repository maintainers will reject code that introduces global variables." The uniqueness could instead be derived per instance (e.g. a random suffix generated in the constructor, similar to how #tmpPath uses crypto.randomBytes).

Prompt for agents
REVIEW.md forbids introducing global variables, but a module-level mutable counter `storageOwnerClientCounter` was added in packages/miniflare/src/index.ts to make client presence names unique across Miniflare instances in one process. Replace it with per-instance uniqueness that doesn't rely on module state, e.g. a random suffix generated in the constructor (the class already uses crypto.randomBytes for #tmpPath).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

QueuesError,
R2_PLUGIN_NAME,
SECRET_STORE_PLUGIN_NAME,
STREAM_PLUGIN_NAME,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Import list is no longer in the enforced order

An imported name is moved out of the enforced alphabetical position in the plugins import list (STREAM_PLUGIN_NAME placed before SERVICE_DEV_REGISTRY_PROXY at packages/miniflare/src/index.ts:63), which breaks the repository's mandated import ordering.
Impact: The formatting check fails, blocking CI until the file is re-formatted.

Rule reference

AGENTS.md → Formatting (oxfmt): "Import order enforced". The specifier list previously had STREAM_PLUGIN_NAME after SOCKET_ENTRY_LOCAL; the diff moves it up between SECRET_STORE_PLUGIN_NAME and SERVICE_DEV_REGISTRY_PROXY and deletes the original entry. Run pnpm prettify to restore the sorted order.

Prompt for agents
In packages/miniflare/src/index.ts the named import `STREAM_PLUGIN_NAME` was moved out of sorted order (now between SECRET_STORE_PLUGIN_NAME and SERVICE_DEV_REGISTRY_PROXY, with the original sorted entry removed). Restore the sorted specifier list (run pnpm prettify).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

✅ All changesets look good

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

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

2 participants