Connecting Cloudflare Workers to external Postgres: Hyperdrive or bust
Direct TCP from Workers to an external Postgres fails with 'proxy request failed, cannot connect'. The fix is Hyperdrive — plus two non-obvious settings (maxUses: 1, a validated-but-unused local password) that aren't in the quickstart.

Your database lives outside Cloudflare — a managed Postgres on Neon, RDS, Supabase, wherever. Your Next.js app now runs on Cloudflare Workers via OpenNext. Connecting the two is not as simple as pasting the connection string.
The symptom#
The same pg + Prisma setup that works everywhere else fails on Workers with:
Error: proxy request failed, cannot connect to the specified addressWorkers can technically open TCP sockets, but in practice direct connections to external Postgres are unreliable-to-impossible depending on provider and region. The supported path is Hyperdrive — Cloudflare's connection pooler that sits between your Worker and the database.
The setup that actually works#
1. The binding in wrangler.jsonc:
"hyperdrive": [
{
"binding": "HYPERDRIVE",
"id": "<your-hyperdrive-config-id>",
"localConnectionString": "postgresql://user:somepassword@localhost:5432/db"
}
]2. Read the connection string at request time, not module scope. Bindings don't exist when the module is evaluated — only inside a request. Our db.ts resolves the string lazily and falls back to DATABASE_URL so next dev and CLI scripts (migrations, seeds) keep working off-platform:
import { getCloudflareContext } from "@opennextjs/cloudflare";
function connectionString(): string {
const onWorkers = typeof navigator !== "undefined" &&
navigator.userAgent === "Cloudflare-Workers";
if (onWorkers) {
const { env } = getCloudflareContext();
return env.HYPERDRIVE.connectionString;
}
return process.env.DATABASE_URL!; // next dev, migrations, scripts
}3. maxUses: 1 on the pg Pool. This is the one that bit us hardest:
const pool = new Pool({ connectionString: connectionString(), maxUses: 1 });Without it, the pool tries to reuse a connection across requests — but on Workers each request is its own isolate lifecycle, and a connection that outlives its request hangs the next one. Symptom: first request fine, second request freezes forever. Hyperdrive is already your pooler; let each Worker request use a connection exactly once and lean on Hyperdrive for the actual pooling.
The gotcha nobody documents#
That localConnectionString in the binding? It's only used by local dev (Miniflare) — the deployed Worker never touches it. But Miniflare validates it at startup, including requiring a password. Leave the password out because "it's never really used" and your local dev server dies with a validation error that says nothing about passwords. Put a real (or dummy-but-well-formed) connection string there.
Takeaways#
- "proxy request failed" from Workers to external Postgres means change the path, not the retry count. Provision Hyperdrive; don't fight direct TCP.
- Resolve bindings per-request. Anything read at module scope is a latent production bug on Workers.
- One request, one connection (
maxUses: 1). Cross-request connection reuse is a Node assumption that doesn't survive the isolate model. - Keep a
DATABASE_URLfallback so migrations and local tooling never depend on the Workers runtime.
Previously in this series: Why we pin Prisma to 6.x on Cloudflare Workers and the revalidatePath layout-scope hang.
Fenix Codex builds and operates production web platforms — get in touch if you need a team that debugs at this depth.