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.
How a query travels
Section titled “How a query travels”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.
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.
Add the provider
Section titled “Add the provider”Each database provider is its own providers() layer. Register it
alongside Cloudflare.providers() in your stack:
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.
import * as Alchemy from "alchemy";import * as Cloudflare from "@alchemy.run/cloudflare";import * as Planetscale from "@alchemy.run/planetscale";import * as Layer from "effect/Layer";
export default Alchemy.Stack( "MyStack", { providers: Cloudflare.providers(), providers: Layer.mergeAll(Cloudflare.providers(), Planetscale.providers()), state: Alchemy.localState(), }, // ...);PlanetScale’s auth uses an API token id + secret.
alchemy profile edit --add Planetscale either reads PLANETSCALE_API_TOKEN_ID,
PLANETSCALE_API_TOKEN, and PLANETSCALE_ORGANIZATION from the
environment (good for CI) or stores the credentials under
~/.alchemy/credentials/<profile>/planetscale-stored.json.
import * as Alchemy from "alchemy";import * as Cloudflare from "@alchemy.run/cloudflare";import * as Planetscale from "@alchemy.run/planetscale";import * as Layer from "effect/Layer";
export default Alchemy.Stack( "MyStack", { providers: Cloudflare.providers(), providers: Layer.mergeAll(Cloudflare.providers(), Planetscale.providers()), state: Alchemy.localState(), }, // ...);PlanetScale’s auth uses an API token id + secret.
alchemy profile edit --add Planetscale either reads PLANETSCALE_API_TOKEN_ID,
PLANETSCALE_API_TOKEN, and PLANETSCALE_ORGANIZATION from the
environment (good for CI) or stores the credentials under
~/.alchemy/credentials/<profile>/planetscale-stored.json.
Provision the database
Section titled “Provision the database”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:
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).
A Planetscale.PostgresDatabase owns the long-lived cluster.
PostgresBranch is a branch off of it (cheap to fork for previews),
and PostgresRole materializes a user + password we can feed to
Hyperdrive:
import * as Planetscale from "@alchemy.run/planetscale";import * as Effect from "effect/Effect";
export const Db = Effect.gen(function* () { const database = yield* Planetscale.PostgresDatabase("app-db", { region: { slug: "us-east" }, clusterSize: "PS_10", });
const branch = yield* Planetscale.PostgresBranch("app-branch", { database, isProduction: false, });
const role = yield* Planetscale.PostgresRole("app-role", { database, branch, inheritedRoles: ["postgres"], });
return { database, branch, role };});The role’s origin (host / port / database / user / password) is
what Hyperdrive consumes — it’s materialized lazily so the actual
password never lands in plan output.
A Planetscale.MySQLDatabase is the long-lived cluster.
MySQLBranch is a branch off of it (cheap to fork for previews),
and MySQLPassword materializes a user + password we can feed to
Hyperdrive:
import * as Planetscale from "@alchemy.run/planetscale";import * as Effect from "effect/Effect";
export const Db = Effect.gen(function* () { const database = yield* Planetscale.MySQLDatabase("app-db", { region: { slug: "us-east" }, clusterSize: "PS_10", allowForeignKeyConstraints: true, });
const branch = yield* Planetscale.MySQLBranch("app-branch", { database, isProduction: false, });
const password = yield* Planetscale.MySQLPassword("app-password", { database, branch, role: "readwriter", });
return { database, branch, password };});The password’s origin (host / port / database / user / password)
is what Hyperdrive consumes — it’s materialized lazily so the
actual secret never lands in plan output.
Put Hyperdrive in front
Section titled “Put Hyperdrive in front”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:
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.
import * as Cloudflare from "@alchemy.run/cloudflare";import * as Planetscale from "@alchemy.run/planetscale";import * as Effect from "effect/Effect";
export const Db = Effect.gen(function* () { // ... return { database, branch, role };});
export const Hyperdrive = Effect.gen(function* () { const { role } = yield* Db; return yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", { origin: role.origin, });});role.origin is the connection target Hyperdrive will pool. The
origin is an Output, so alchemy orders PostgresDatabase →
PostgresBranch → PostgresRole → Hyperdrive on the deploy graph.
import * as Cloudflare from "@alchemy.run/cloudflare";import * as Planetscale from "@alchemy.run/planetscale";import * as Effect from "effect/Effect";
export const Db = Effect.gen(function* () { // ... return { database, branch, password };});
export const Hyperdrive = Effect.gen(function* () { const { password } = yield* Db; return yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", { origin: password.origin, });});password.origin is the connection target Hyperdrive will pool —
its scheme is "mysql", so Hyperdrive provisions a MySQL pooler.
The origin is an Output, so alchemy orders MySQLDatabase →
MySQLBranch → MySQLPassword → Hyperdrive on the deploy graph.
Dev mode and the pooled origin
Section titled “Dev mode and the pooled origin”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:
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.
export const Hyperdrive = Effect.gen(function* () { const { role } = yield* Db; return yield* Cloudflare.Hyperdrive.Connection("app-hyperdrive", { origin: role.origin, dev: role.pooledOrigin, });});role.pooledOrigin targets PSBouncer on port 6432 — the direct
role.origin uses 5432 — and the raw pooled URL is available as
role.connectionUrlPooled. Deployed Workers still go through
Hyperdrive at the direct endpoint; local dev goes through
PlanetScale’s pooler.
MySQLPassword has no pooled variant — when dev is omitted the
binding falls back to password.origin automatically, so there is
nothing to add. Set dev only when local dev should hit a
different database, e.g. a MySQL running on localhost.
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.
Install the client library
Section titled “Install the client library”The Worker queries Hyperdrive with Effect SQL. Install the driver that matches your engine:
@effect/sql-pg — Effect’s native Postgres driver:
pnpm add https://pkg.alchemy.run/@alchemy.run/sql/pr:2930:a10573e @effect/sql-pgbun add https://pkg.alchemy.run/@alchemy.run/sql/pr:2930:a10573e @effect/sql-pgnpm install https://pkg.alchemy.run/@alchemy.run/sql/pr:2930:a10573e @effect/sql-pgyarn add https://pkg.alchemy.run/@alchemy.run/sql/pr:2930:a10573e @effect/sql-pg@effect/sql-pg — Effect’s native Postgres driver:
pnpm add https://pkg.alchemy.run/@alchemy.run/sql/pr:2930:a10573e @effect/sql-pgbun add https://pkg.alchemy.run/@alchemy.run/sql/pr:2930:a10573e @effect/sql-pgnpm install https://pkg.alchemy.run/@alchemy.run/sql/pr:2930:a10573e @effect/sql-pgyarn add https://pkg.alchemy.run/@alchemy.run/sql/pr:2930:a10573e @effect/sql-pg@effect/sql-mysql2 — Effect’s MySQL client:
pnpm add https://pkg.alchemy.run/@alchemy.run/sql/pr:2930:a10573e @effect/sql-mysql2bun add https://pkg.alchemy.run/@alchemy.run/sql/pr:2930:a10573e @effect/sql-mysql2npm install https://pkg.alchemy.run/@alchemy.run/sql/pr:2930:a10573e @effect/sql-mysql2yarn add https://pkg.alchemy.run/@alchemy.run/sql/pr:2930:a10573e @effect/sql-mysql2Bind Hyperdrive to your Worker
Section titled “Bind Hyperdrive to your Worker”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:
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)),) {}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)),) {}import * as Cloudflare from "@alchemy.run/cloudflare";import * as SQL from "@alchemy.run/sql/MySQL";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.MySQL({ 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.
Wire the resources into the stack
Section titled “Wire the resources into the stack”Update alchemy.run.ts to yield Db and Hyperdrive so they
become part of the deploy graph:
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>() }; }),);Deploy
Section titled “Deploy”pnpm alchemy deploybun alchemy deploynpx alchemy deployyarn alchemy deployWatch 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.
Workers defaults
Section titled “Workers defaults”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.
Where next
Section titled “Where next”Guides that build on Hyperdrive:
- Private database (Workers VPC) — reach a database with no public endpoint, such as Aurora in an AWS VPC, through a Cloudflare Tunnel.
- Add Drizzle ORM — add typed schemas and alchemy-managed migrations.
- Shared database across stages — point PR-preview stages at one long-lived database.
- Branch from a shared database — copy-on-write branches per preview stage.
Database integrations: