Summary
When binding a Cloudflare.Container to a Cloudflare.DurableObjectNamespace via Cloudflare.Container.bind inside a DO class body, the container application is created in Cloudflare's control plane without its Durable Object namespace linkage. Runtime consequence:
Error: There is no container application assigned to this Durable Object namespace
The container is endlessly restarted by StartContainer.ts's exponential retry loop, silently masking the real error.
Environment
alchemy@2.0.0-beta.9 (reproduces on fork yovanoc/alchemy-effect-fork@2282ff3, same code on upstream main)
effect@4.0.0-beta.48
@cloudflare/workers-types latest
- Bun runtime, macOS darwin
- Pattern: canonical Container Layer — class +
.make() in separate files, DO imports class
Reproduction
Canonical pattern from the Container docstring:
// src/containers/device.ts
export class DeviceContainer extends Cloudflare.Container<DeviceContainer>()("DeviceContainerApp", {
main: "./src/containers/device/server/server.ts",
runtime: "bun",
instanceType: "dev",
}) {}
// src/containers/device-do.ts
export default class DeviceContainerDO extends Cloudflare.DurableObjectNamespace<DeviceContainerDO>()(
"DeviceContainer",
Effect.gen(function* () {
const container = yield* Cloudflare.Container.bind(DeviceContainer);
return Effect.gen(function* () {
const started = yield* Cloudflare.start(container);
const port = yield* started.getTcpPort(3000);
return {
fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
return yield* port.fetch(request);
}),
};
});
}),
) {}
Deploy succeeds cleanly. Any invocation of the DO that reaches Cloudflare.start(container) triggers the runtime error above.
Evidence
Alchemy state file .alchemy/state/<app>/<stage>/DeviceContainerApp.json:
{
"attr": {
"applicationId": "a03a9b73-d707-42bb-b301-1f61457d943a",
"durableObjects": null // ← should contain { namespaceId: "..." }
},
"bindings": [
{
"sid": "DeviceContainer",
"data": {
"durableObjects": {} // ← should contain { namespaceId: "<resolved>" }
}
}
]
}
Worker tail logs repeat every ~2 min:
INFO (#41): Container not running, starting...
INFO (#41): Container started, launching monitor
INFO (#53): Container monitor exited
Error There is no container application assigned to this Durable Object namespace
Root Cause Analysis
A circular Output dependency between Container and DurableObjectNamespace:
-
DurableObjectNamespace.ts:585-596 — self.namespaceId is an Output<string> that resolves via worker.durableObjectNamespaces.pipe(Output.map(ns => ns?.[name])). It resolves only after the Worker has deployed (Worker is what populates durableObjectNamespaces).
-
ContainerBinding.ts:32-36 — bindContainer calls:
yield* container.bind`${namespace}`({
durableObjects: {
namespaceId: namespace.namespaceId, // Output<string>, unresolved
},
});
-
Apply.ts:451 — When the ContainerApplication is about to create, Output.evaluate(node.bindings, outputs) runs. But the Container is an upstream dependency of the Worker (Worker binds to Container). So at Container-create time, the Worker hasn't deployed and worker.durableObjectNamespaces[name] is still undefined. Output.evaluate resolves namespaceId to undefined.
-
ContainerApplication.ts:871-888 — getDurableObjects(bindings):
const dos = bindings.flatMap((b) =>
b.data.durableObjects ? [b.data.durableObjects] : [],
);
if (dos.length === 0) return Effect.succeed(undefined);
if (dos.length === 1) return Effect.succeed(dos[0]);
b.data.durableObjects = { namespaceId: undefined } is truthy, so the function returns { namespaceId: undefined } instead of undefined.
-
ContainerApplication.ts:1038 — createApplication is called with durableObjects: { namespaceId: undefined }, which serializes the namespaceId field away. Cloudflare's API receives a malformed/empty DO attachment and creates the app without linkage.
-
ContainerApplication.ts:985-988 — The recreate-on-DO-change guard:
if (output && !adoptPolicy && !deepEqual(output.durableObjects, durableObjects)) { ... }
After the first deploy, output.durableObjects === null and durableObjects === { namespaceId: undefined }. Subsequent deploys hit this check: both sides deep-equal to "nothing meaningful" under JSON round-tripping, so the guard never fires and the broken state persists across all future alchemy deploy runs (including --force).
Intent vs Reality
The precreate/create split at ContainerApplication.ts:933-966 includes this comment:
Precreate intentionally omits the Durable Object attachment so the worker can bind to this application id and break the circular dependency. The final create step recreates the application with the resolved namespace when needed.
The intent is correct: precreate without DO linkage → Worker deploys → Worker populates durableObjectNamespaces → create re-runs with resolved namespaceId → recreate the app with linkage.
But there is no ordering mechanism that re-evaluates the Container's bindings after Worker deploy. Apply.ts evaluates bindings once at create time against currently-resolved outputs. Because Container is upstream of Worker in the dependency graph, the Worker's durableObjectNamespaces is never available when Container's bindings are evaluated.
Suggested Fix Directions
Not submitting a patch because the fix likely requires a framework-level decision, but options:
-
Make DurableObjectNamespace declare the container linkage directly. Instead of having bindContainer push { namespaceId: <unresolvable Output> } into the container's bindings, have DurableObjectNamespace itself (which knows its own namespaceId resolution path) push the linkage. This inverts the dependency so the Output producer (Worker/DONamespace) is downstream of its consumer (Container's DO attachment).
-
Two-phase container create: after Worker deploys and DO namespace IDs are known, re-invoke the Container provider's update path with the now-resolved bindings to patch in the DO linkage. Requires Apply.ts changes to support a "post-worker" re-evaluation pass.
-
Fail loudly at create time: if getDurableObjects(bindings) returns a shape with namespaceId === undefined, Effect.die with a clear message instead of silently serializing a broken payload. This at least surfaces the bug during alchemy deploy instead of only at runtime.
Happy to contribute a patch once the preferred approach is agreed upon.
Workaround
None found. Manual CF API attachment of DO namespace to container application drifts on next deploy (alchemy state overwrites). Downgrading to Worker-only architecture (no containers) avoids the path entirely.
Related Files
packages/alchemy/src/Cloudflare/Container/ContainerBinding.ts (lines 32-36)
packages/alchemy/src/Cloudflare/Container/ContainerApplication.ts (lines 871-888, 933-1045)
packages/alchemy/src/Cloudflare/Workers/DurableObjectNamespace.ts (lines 585-596)
packages/alchemy/src/Apply.ts (line 451)
Summary
When binding a
Cloudflare.Containerto aCloudflare.DurableObjectNamespaceviaCloudflare.Container.bindinside a DO class body, the container application is created in Cloudflare's control plane without its Durable Object namespace linkage. Runtime consequence:The container is endlessly restarted by
StartContainer.ts's exponential retry loop, silently masking the real error.Environment
alchemy@2.0.0-beta.9(reproduces on forkyovanoc/alchemy-effect-fork@2282ff3, same code on upstreammain)effect@4.0.0-beta.48@cloudflare/workers-typeslatest.make()in separate files, DO imports classReproduction
Canonical pattern from the Container docstring:
Deploy succeeds cleanly. Any invocation of the DO that reaches
Cloudflare.start(container)triggers the runtime error above.Evidence
Alchemy state file
.alchemy/state/<app>/<stage>/DeviceContainerApp.json:{ "attr": { "applicationId": "a03a9b73-d707-42bb-b301-1f61457d943a", "durableObjects": null // ← should contain { namespaceId: "..." } }, "bindings": [ { "sid": "DeviceContainer", "data": { "durableObjects": {} // ← should contain { namespaceId: "<resolved>" } } } ] }Worker tail logs repeat every ~2 min:
Root Cause Analysis
A circular Output dependency between
ContainerandDurableObjectNamespace:DurableObjectNamespace.ts:585-596—self.namespaceIdis anOutput<string>that resolves viaworker.durableObjectNamespaces.pipe(Output.map(ns => ns?.[name])). It resolves only after the Worker has deployed (Worker is what populatesdurableObjectNamespaces).ContainerBinding.ts:32-36—bindContainercalls:Apply.ts:451— When the ContainerApplication is about to create,Output.evaluate(node.bindings, outputs)runs. But the Container is an upstream dependency of the Worker (Worker binds to Container). So at Container-create time, the Worker hasn't deployed andworker.durableObjectNamespaces[name]is stillundefined.Output.evaluateresolvesnamespaceIdtoundefined.ContainerApplication.ts:871-888—getDurableObjects(bindings):b.data.durableObjects = { namespaceId: undefined }is truthy, so the function returns{ namespaceId: undefined }instead ofundefined.ContainerApplication.ts:1038—createApplicationis called withdurableObjects: { namespaceId: undefined }, which serializes thenamespaceIdfield away. Cloudflare's API receives a malformed/empty DO attachment and creates the app without linkage.ContainerApplication.ts:985-988— The recreate-on-DO-change guard:After the first deploy,
output.durableObjects === nullanddurableObjects === { namespaceId: undefined }. Subsequent deploys hit this check: both sides deep-equal to "nothing meaningful" under JSON round-tripping, so the guard never fires and the broken state persists across all futurealchemy deployruns (including--force).Intent vs Reality
The
precreate/createsplit atContainerApplication.ts:933-966includes this comment:The intent is correct: precreate without DO linkage → Worker deploys → Worker populates
durableObjectNamespaces→createre-runs with resolved namespaceId → recreate the app with linkage.But there is no ordering mechanism that re-evaluates the Container's bindings after Worker deploy.
Apply.tsevaluates bindings once at create time against currently-resolved outputs. Because Container is upstream of Worker in the dependency graph, the Worker'sdurableObjectNamespacesis never available when Container's bindings are evaluated.Suggested Fix Directions
Not submitting a patch because the fix likely requires a framework-level decision, but options:
Make
DurableObjectNamespacedeclare the container linkage directly. Instead of havingbindContainerpush{ namespaceId: <unresolvable Output> }into the container's bindings, haveDurableObjectNamespaceitself (which knows its ownnamespaceIdresolution path) push the linkage. This inverts the dependency so the Output producer (Worker/DONamespace) is downstream of its consumer (Container's DO attachment).Two-phase container create: after Worker deploys and DO namespace IDs are known, re-invoke the Container provider's
updatepath with the now-resolved bindings to patch in the DO linkage. Requires Apply.ts changes to support a "post-worker" re-evaluation pass.Fail loudly at create time: if
getDurableObjects(bindings)returns a shape withnamespaceId === undefined,Effect.diewith a clear message instead of silently serializing a broken payload. This at least surfaces the bug duringalchemy deployinstead of only at runtime.Happy to contribute a patch once the preferred approach is agreed upon.
Workaround
None found. Manual CF API attachment of DO namespace to container application drifts on next deploy (alchemy state overwrites). Downgrading to Worker-only architecture (no containers) avoids the path entirely.
Related Files
packages/alchemy/src/Cloudflare/Container/ContainerBinding.ts(lines 32-36)packages/alchemy/src/Cloudflare/Container/ContainerApplication.ts(lines 871-888, 933-1045)packages/alchemy/src/Cloudflare/Workers/DurableObjectNamespace.ts(lines 585-596)packages/alchemy/src/Apply.ts(line 451)