Cloudflare Workers error
Error: Cannot perform I/O on behalf of a different request
"Error: Cannot perform I/O on behalf of a different request" means a Worker used an object that belongs to another request: a stream, a Request or Response body, or a connection opened while an earlier request ran. Cloudflare names caching a Response in global scope as the usual cause, and developers report the same error with a Postgres client created once at module level against Supabase. The first request works and a later one fails. Create the client inside the fetch handler for every request, and cache plain data such as text, not the Response. Cloudflare's Hyperdrive docs say Hyperdrive keeps the connection pool, so a new client per request is fast. Reproduced with wrangler 4.156.0 on 2026-10-31.
What does the full error say, and why only on the second request?
We stored a fetch() Response in a variable at module level during the first request, then read its body in the second. The first request answered 200 and the second answered 500 with the message below. The object works in the request that made it, so the error appears only when a later request touches it.
Error: Cannot perform I/O on behalf of a different request. I/O objects (such as streams, request/response bodies, and others) created in the context of one request handler cannot be accessed from a different request's handler. This is a limitation of Cloudflare Workers which allows us to improve overall performance. (I/O type: ReadableStreamSource>)How do I connect to Supabase Postgres from a Worker without a global client?
The pattern behind the error is a client created once and reused, as in the first block below. The Cloudflare Supabase guide creates the supabase-js client inside the fetch handler with createClient(env.SUPABASE_URL, env.SUPABASE_KEY). For direct Postgres, the guide points to Hyperdrive with a driver such as node-postgres or Postgres.js, and the Hyperdrive Postgres.js example creates the client inside fetch on each request. That example needs the nodejs_compat flag, which Cloudflare says database drivers require.
// reused across requests, fails on the second one
let sql
export default {
async fetch(request, env) {
sql ??= postgres(env.HYPERDRIVE.connectionString)
return Response.json(await sql.unsafe("select * from notes"))
},
}
// one client per request
export default {
async fetch(request, env) {
const sql = postgres(env.HYPERDRIVE.connectionString)
return Response.json(await sql.unsafe("select * from notes"))
},
}Can I cache something between requests?
Cache the data, not the I/O object. Cloudflare's example keeps the response body as text in a global variable and builds a new Response from that text on each request. For state shared across requests, the docs point to Durable Objects, and for cached data to Workers KV.
let cachedText
export default {
async fetch(request) {
if (!cachedText) {
const upstream = await fetch("https://example.com/data")
cachedText = await upstream.text()
}
return new Response(cachedText)
},
}Checklist
How to fix it, then check the fix
- 01Find clients and responses created outside a handler:
grep -rnE "^(let|const|var) .*(postgres\(|new Pool|new Client|createClient\(|await fetch\()" --exclude-dir=node_modules src - 02Move each client into the fetch handler, so every request builds its own. Keep module-level code to plain values.
- 03For a direct Postgres connection to Supabase, create a Hyperdrive configuration from the connection string, in the dashboard or with wrangler hyperdrive create, add the binding to your Wrangler config, and add the nodejs_compat flag.
- 04Replace any cached Response with its text or JSON, and build a new Response each time.
- 05Redeploy, then call the same route twice in a row. Both calls answer normally, where before the second one returned a 500.
Related
Related guides
Sources
Official docs behind this checklist
Browse all launch guides · Read resources · How to make and check your next change
FAQ
FAQ for builders before launch
Does a per-request client open a new database connection every time?
The Hyperdrive docs say Hyperdrive maintains the underlying connection pool, so creating a new client on each request is fast and recommended. Without Hyperdrive that guarantee does not apply, and you should check your database pooler limits.
Is a global supabase-js client also a problem?
The Cloudflare Supabase guide creates it inside the fetch handler, and its page gives no guidance on reusing one. Follow the guide and create it per request. The reports of this error that we found involve Postgres clients and cached Response objects.
Where do I keep state that all requests share?
The Cloudflare error docs recommend Durable Objects for shared state and Workers KV for caching data across requests.