Skip to content

Hyperdrive

A Worker can talk to Postgres or MySQL, but opening a database connection takes several round trips, and a Worker in Sydney talking to a database in Virginia pays for each one, on every request. Hyperdrive removes that cost. It keeps a pool of open connections next to your database, and gives the Worker a connection string that reaches it over Cloudflare’s network.

The Worker connects to Hyperdrive in its own city, which is quick. Hyperdrive sends the query to its pool near the database, which already holds open connections, and the query runs at once.

Cloudflare's networkWorker → Hyperdriveboth in Sydneypool → Postgreswarm connections, Virginia
The Worker connects to Hyperdrive in its own city. The query crosses the ocean once, to a pool that already holds connections next to the database.

Hyperdrive can also cache the results of read queries, so repeated reads don’t reach the database at all. To your code it’s an ordinary connection string, so any Postgres or MySQL driver works.

Use Hyperdrive for a database that lives outside Cloudflare: one on Neon or PlanetScale, or one you run yourself, even in a private network. For a database inside Cloudflare, use D1.

The steps below work for Neon, PlanetScale Postgres and PlanetScale MySQL. Pick one, and the rest of the page follows your choice.

Each database provider is its own providers() layer. Register it alongside Cloudflare.providers() in your stack:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Cloudflare from "@alchemy.run/cloudflare";
import * as Neon from "@alchemy.run/neon";
import * as Layer from "effect/Layer";
export default Alchemy.Stack(
"MyStack",
{
providers: Cloudflare.providers(),
providers: Layer.mergeAll(Cloudflare.providers(), Neon.providers()),
state: Alchemy.localState(),
},
// ...
);

Neon’s auth is API-key-based. After this change, alchemy profile edit --add Neon either reads NEON_API_KEY from the environment (good for CI) or stores a key under ~/.alchemy/credentials/<profile>/neon-stored.json.

Create src/Db.ts. The shape is the same across providers — a top-level database resource, a branch, and (for PlanetScale) a credentials resource that owns the password.

A Neon.Project is the top-level container. It owns a default branch, a default role, and the WAL history that copy-on-write branches fork from:

src/Db.ts
import * as Neon from "@alchemy.run/neon";
import * as Effect from "effect/Effect";
export const Db = Effect.gen(function* () {
const project = yield* Neon.Project("app-db", {
region: "aws-us-east-1",
});
const branch = yield* Neon.Branch("app-branch", {
project,
});
return { project, branch };
});

Neon.Branch is a copy-on-write fork — cheap to create, fast to destroy, and ideal for preview environments. Each branch has its own connection string (branch.connectionUri) and a pooled variant (branch.pooledConnectionUri).

Hyperdrive needs an origin describing where your database lives. Each provider exposes a pre-parsed origin output ({ scheme, host, port, database, user, password }) ready to feed straight in:

src/Db.ts
import * as Cloudflare from "@alchemy.run/cloudflare";
import * as Neon from "@alchemy.run/neon";
import * as Effect from "effect/Effect";
export const Db = Effect.gen(function* () {
const project = yield* Neon.Project("app-db", { region: "aws-us-east-1" });
const branch = yield* Neon.Branch("app-branch", { project });
return { project, branch };
});
export const Hyperdrive = Effect.gen(function* () {
const { branch } = yield* Db;
return yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", {
origin: branch.origin,
});
});

branch.origin points at the direct (non-pooled) Neon endpoint — the recommended target when sitting behind Hyperdrive, since Hyperdrive already does its own connection pooling.

In alchemy dev, alchemy doesn’t create a Hyperdrive config at all — the local Worker gets a connection string that points straight at the database. With Hyperdrive out of the path, nothing pools those local connections, so point the dev override at the provider’s own pooled endpoint instead:

src/Db.ts
export const Hyperdrive = Effect.gen(function* () {
const { branch } = yield* Db;
return yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", {
origin: branch.origin,
dev: branch.pooledOrigin,
});
});

branch.pooledOrigin is the same connection parsed against Neon’s pgbouncer endpoint (the raw URI is branch.pooledConnectionUri). Deployed Workers still go through Hyperdrive at the direct endpoint; local dev goes through Neon’s pooler.

The pooled endpoint is also the right target when Hyperdrive isn’t in the picture at all. A Hyperdrive connection string only exists inside a deployed Worker — the binding resolves it at runtime — so anything else that talks to the database (CI jobs, a container, psql on your laptop) should connect to the pooled URI directly: branch.pooledConnectionUri on Neon, role.connectionUrlPooled on PlanetScale. For a container that means passing the pooled URI in as an environment variable — see Connect to a SQL database. Just don’t do the reverse — origin deliberately points Hyperdrive at the direct endpoint, because Hyperdrive already pools, and stacking it on top of the provider’s pooler means two poolers fighting over the same connections.

The Worker queries Hyperdrive with Effect SQL. Install the driver that matches your engine:

@effect/sql-pg — Effect’s native Postgres driver:

Terminal window
pnpm add https://pkg.alchemy.run/@alchemy.run/sql/pr:2930:a10573e @effect/sql-pg

Cloudflare.Hyperdrive.Connect(...) returns a typed accessor for the runtime binding — connection string, host, port, user, password, database — plus a raw escape hatch for libraries that want the underlying Hyperdrive object. Pass its connectionString to Effect SQL:

src/Api.ts
import * as Cloudflare from "@alchemy.run/cloudflare";
import * as SQL from "@alchemy.run/sql/Postgres";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
import { Hyperdrive } from "./Db.ts";
export default class Api extends Cloudflare.Worker<Api>()(
"Api",
{
main: import.meta.url,
compatibility: {
// The driver opens its socket through Node's `net` and `tls` APIs.
flags: ["nodejs_compat"],
},
},
Effect.gen(function* () {
const hd = yield* Cloudflare.Hyperdrive.Connect(Hyperdrive);
const sql = yield* SQL.Postgres({ url: hd.connectionString });
return {
fetch: Effect.gen(function* () {
const rows = yield* sql`SELECT now() AS now`;
return yield* HttpServerResponse.json({ ok: true, rows });
}),
};
}),
}).pipe(Effect.provide(Cloudflare.Hyperdrive.ConnectBinding)),
) {}

SQL.Postgres and SQL.MySQL open a pool lazily inside each request’s scope and close it when the request ends. Hyperdrive does the pooling on Cloudflare’s side, so the Worker never retains sockets across requests. The Postgres and MySQL client guides cover queries, typed errors, and transactions; Drizzle adds typed schemas on the same connection.

Update alchemy.run.ts to yield Db and Hyperdrive so they become part of the deploy graph:

alchemy.run.ts
import Api from "./src/Api.ts";
import { Db, Hyperdrive } from "./src/Db.ts";
export default Alchemy.Stack(
"MyStack",
{ /* ... */ },
Effect.gen(function* () {
yield* Db;
yield* Hyperdrive;
const api = yield* Api;
return { url: api.url.as<string>() };
}),
);
Terminal window
pnpm alchemy deploy

Watch the deploy plan: alchemy creates the database, then the branch, then the credentials (for PlanetScale), then the Hyperdrive that points at the connection origin, then the Worker with the Hyperdrive binding attached. After it completes, hit your Worker URL and you should see something like:

{ "ok": true, "rows": [{ "now": "2026-05-04T07:53:48.123Z" }] }

You’re now talking to your database from the edge through Hyperdrive.

SQL.MySQL and Drizzle.MySQL disable prepared statements and mysql2’s eval-based row parsers when running in workerd. Hyperdrive’s MySQL proxy uses the text protocol, and Workers prohibit runtime JavaScript compilation.

const sql = yield* SQL.MySQL({
url: connection.connectionString,
disablePreparedStatements: true,
poolConfig: { disableEval: true },
});

These are detected defaults, not required boilerplate. Explicit configuration wins; direct connections outside workerd retain prepared statements and normal parsers by default. The adapter parses URL fields and query parameters into the driver configuration so poolConfig options reach mysql2.

Guides that build on Hyperdrive:

Database integrations: