Summary
Summary
modal@0.9.0 cannot run inside a Cloudflare Worker (or Durable Object) — the same edge/V8-isolate constraint that affects Vercel Edge, Deno Deploy, etc. It works fine in a Node process, including a Cloudflare Container (full Linux/Node runtime). The blocker is the gRPC transport: nice-grpc → @grpc/grpc-js speaks gRPC over HTTP/2 with bidirectional streaming, which the Workers runtime does not support even with nodejs_compat + node:http2 enabled.
Is a Workers-compatible (fetch/Connect/gRPC-Web) transport on the roadmap?
Environment
modal 0.9.0 (JS/TS SDK)
- Target runtime: Cloudflare Workers / Durable Objects (
workerd), nodejs_compat, compat date ≥ 2026-03-17 (so enable_nodejs_http2_module is auto-on)
- Auth: explicit
MODAL_TOKEN_ID / MODAL_TOKEN_SECRET (no ~/.modal.toml)
What happens
Instantiating a client / making any RPC fails inside the Worker: @grpc/grpc-js cannot establish a working HTTP/2 channel to api.modal.com. The SDK also imports node:fs/node:os (config loading), which are unavailable/partial on the edge, but those are avoidable by passing tokens explicitly — the transport is the hard blocker.
Root cause
- The SDK's transport is Node gRPC:
dependencies include nice-grpc and protobufjs; the built bundle imports node:fs, node:fs/promises, node:os, node:crypto, node:stream, and calls createChannel / ChannelCredentials from nice-grpc.
nice-grpc (Node variant) runs on @grpc/grpc-js, which requires HTTP/2 with client-streaming / bidirectional streaming.
- Cloudflare Workers do not support outbound HTTP/2 bidirectional streaming. Even with a compat date ≥ 2026-03-17 (where
enable_nodejs_http2_module auto-enables node:http2), that module is stubbed and bidi streaming is unimplemented. Tracked upstream:
- grpc-js also opens raw
node:net / node:tls sockets; Workers only permit outbound connections via fetch() or connect() from cloudflare:sockets, which grpc-js does not use.
Why it works in a Cloudflare Container (our current workaround)
A Cloudflare Container is a real Linux/Node runtime with functional http2/net/tls/fs, so nice-grpc/grpc-js behave exactly as on any Node server. We run the Modal-facing orchestrator in a Container fronted by a Durable Object and bind it into the Worker, instead of calling Modal directly from the Worker. It works, but it means we can't do the lightweight thing (call Modal straight from an edge Worker) and pay for a container to sit in the request path.
Possible fixes (SDK side)
- A fetch-based / Connect transport. A Connect protocol or
nice-grpc-web transport over standard fetch would run on Workers (and every edge runtime + browsers). This requires Modal's control plane to expose a gRPC-Web/Connect endpoint (Connect-native server or an Envoy/grpc-web proxy). Caveat: gRPC-Web/Connect support unary + server-streaming but not client/bidi streaming — so any RPCs Modal relies on for bidi streams (interactive exec, log tailing) would need a server-streaming or chunked-fetch fallback.
- Pluggable transport + runtime detection. Let callers inject a transport, and make config file loading (
node:fs/node:os) lazy/optional so merely importing the SDK with explicit tokens doesn't touch the filesystem. That at least unblocks bundling on the edge.
- Document the constraint. A short note in the JS SDK docs ("runs on Node ≥ 22; not on edge/isolate runtimes — gRPC/HTTP2 transport") would save others the investigation.
Fix (Cloudflare side, out of Modal's hands)
Track cloudflare/workerd#6455 (HTTP/2 bidi streaming). If/when that lands, grpc-js may work under nodejs_compat with no SDK change.
Related
Version
0.9.0
App ID
No response
Summary
Summary
modal@0.9.0cannot run inside a Cloudflare Worker (or Durable Object) — the same edge/V8-isolate constraint that affects Vercel Edge, Deno Deploy, etc. It works fine in a Node process, including a Cloudflare Container (full Linux/Node runtime). The blocker is the gRPC transport:nice-grpc→@grpc/grpc-jsspeaks gRPC over HTTP/2 with bidirectional streaming, which the Workers runtime does not support even withnodejs_compat+node:http2enabled.Is a Workers-compatible (fetch/Connect/gRPC-Web) transport on the roadmap?
Environment
modal0.9.0 (JS/TS SDK)workerd),nodejs_compat, compat date ≥ 2026-03-17 (soenable_nodejs_http2_moduleis auto-on)MODAL_TOKEN_ID/MODAL_TOKEN_SECRET(no~/.modal.toml)What happens
Instantiating a client / making any RPC fails inside the Worker:
@grpc/grpc-jscannot establish a working HTTP/2 channel toapi.modal.com. The SDK also importsnode:fs/node:os(config loading), which are unavailable/partial on the edge, but those are avoidable by passing tokens explicitly — the transport is the hard blocker.Root cause
dependenciesincludenice-grpcandprotobufjs; the built bundle importsnode:fs,node:fs/promises,node:os,node:crypto,node:stream, and callscreateChannel/ChannelCredentialsfromnice-grpc.nice-grpc(Node variant) runs on@grpc/grpc-js, which requires HTTP/2 with client-streaming / bidirectional streaming.enable_nodejs_http2_moduleauto-enablesnode:http2), that module is stubbed and bidi streaming is unimplemented. Tracked upstream:node:net/node:tlssockets; Workers only permit outbound connections viafetch()orconnect()fromcloudflare:sockets, which grpc-js does not use.Why it works in a Cloudflare Container (our current workaround)
A Cloudflare Container is a real Linux/Node runtime with functional
http2/net/tls/fs, sonice-grpc/grpc-jsbehave exactly as on any Node server. We run the Modal-facing orchestrator in a Container fronted by a Durable Object and bind it into the Worker, instead of calling Modal directly from the Worker. It works, but it means we can't do the lightweight thing (call Modal straight from an edge Worker) and pay for a container to sit in the request path.Possible fixes (SDK side)
nice-grpc-webtransport over standardfetchwould run on Workers (and every edge runtime + browsers). This requires Modal's control plane to expose a gRPC-Web/Connect endpoint (Connect-native server or an Envoy/grpc-web proxy). Caveat: gRPC-Web/Connect support unary + server-streaming but not client/bidi streaming — so any RPCs Modal relies on for bidi streams (interactive exec, log tailing) would need a server-streaming or chunked-fetch fallback.node:fs/node:os) lazy/optional so merely importing the SDK with explicit tokens doesn't touch the filesystem. That at least unblocks bundling on the edge.Fix (Cloudflare side, out of Modal's hands)
Track cloudflare/workerd#6455 (HTTP/2 bidi streaming). If/when that lands,
grpc-jsmay work undernodejs_compatwith no SDK change.Related
modal run/ Modal SDK unusable from Claude Code Web due to gRPC sandbox restrictions #4113 — "Modal SDK unusable from Claude Code Web due to gRPC sandbox restrictions" (different environment, same gRPC root cause)Version
0.9.0
App ID
No response