Cloudflare Workers and Hyperdrive with TanStack Start
Cloudflare Workers and Hyperdrive with TanStack Start êŽë š
Welcome to Part 2 of our series on using Cloudflare for Web Apps (and specifically TanStack Start). In Part 1, we covered the absolute basics. We deployed a web app to Cloudflare, saw how our Wrangler file works, set up some secrets, and we saw Cloudflare generate typings to keep TypeScript happy.
Article Series
Now, weâll set up a database. Weâll look at Hyperdrive and why itâs needed, as well as some potentially counterintuitive ways we need to set up our database object (or any I/O object). Weâll use TanStack Start specifically here, but these principles apply to any web framework, though some implementation details might differ slightly.
Preliminaries
I love Drizzle and use it for all my projects. Itâs an outstanding, unique ORM thatâs essentially a thin TypeScript layer on top of SQL. Iâve written about it here and here.
Iâll be using Postgres, so weâll also install some utilities.
npm i drizzle-orm@rc drizzle-kit@rc pg @types/pg
Next, add a drizzle.config.ts file.
import { defineConfig } from "drizzle-kit";
const connectionString = process.env.POSTGRES!;
export default defineConfig({
dialect: "postgresql",
dbCredentials: {
url: connectionString,
},
out: "./src/drizzle",
});
Then run:
npx drizzle-kit pull
That will generate our Drizzle schema. We wonât cover those specifics here. See the Drizzle posts above if youâre curious, but really you can query your data however you want; for the purposes of this post, it makes no difference which, if any, ORM you use.
Adam Rackis From Spotify
The Wrong Way (for Cloudflare)
For now, letâs do something fairly common that usually works well enough. Weâll add a db.ts module, with this code
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.POSTGRES!,
});
export const db = drizzle({ client: pool });
Again, this has nothing to do with Drizzle. Export a connection to your database however youâd like. The issues will be the same.
Issue 1: Performance
Remember, Cloudflare workers spin up very quickly, on demand, as needed to serve requests. As these (potentially numerous) workers come into existence, each of them establishing a connection to your database poses two problems.
The first is performance. Opening a fresh database connection is a relatively slow operation. We donât want that happening every time a worker spins up. This is not a concern limited to Cloudflare; any cloud function solution would have the same problem. A web application running atop AWS Lambda would not want to exacerbate existing cold starts by adding TCP database connection overhead, and, of course, low-latency Cloudflare workers would not want to introduce cold-start characteristics in this way.
The second is the sheer number of connections that would be stood up in this way. Again, this applies to any platform that works via cloud functions. As your app grows in traffic, the number of cloud functions (Cloudflare workers, AWS Lambda, etc) would grow to a large number, as would the number of connections open on your database. And databases always have a limit on the number of connections they support.
This is, of course, a solved problem. Solutions like PgBouncer pool pre-warm connections and act as a proxy to your database. Your application connects to PgBouncer, and PgBouncer provides an open connection. Cloudflare provides its own version of this called Hyperdrive, which weâll look at shortly.
Issue 2: Per-Request Cleanup
Hereâs this code again:
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.POSTGRES!,
});
export const db = drizzle({ client: pool });
The second issue with this code is that it violates Cloudflareâs rule on what is allowed to persist between requests. Long-lived I/O resources such as Node.js connection pools do not fit the Workers execution model and can trigger runtime errors like this

Letâs solve both of these problems.
Hyperdrive
No matter how we create our database object in code, we donât want to connect directly to our source database; we want to connect to a pre-warmed connection pool. Cloudflare provides one for us called Hyperdrive. To get started, go to the Cloudflare dashboard, and under Storage and databases, find the option for âPostgres & MySQL (Hyperdrive)â.

Amusingly, the Hyperdrive in the menu option may be truncated with how they display it.
Hit the connect to database button.

Youâll be greeted with a few options for how to proceed. For this post, Iâll be using PlanetScale.

Follow the prompts, authenticate if needed, select your database, and most importantly, be sure to fill in your database name; you almost certainly do not want the default value of the postgres.

Once complete, you should be greeted with a new Wrangler entry.

Thatâs what mine looks like, and no, thereâs nothing secret or private about that data. In fact, youâll need it in your Wrangler file and to have committed in Git if you want Cloudflareâs GitHub integration to work.
Copy that into your Wrangler file.
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "fitness-tracker",
"compatibility_date": "2025-09-02",
"compatibility_flags": ["nodejs_compat"],
"main": "@tanstack/react-start/server-entry",
"hyperdrive": [
{
"binding": "HYPERDRIVE",
"id": "dd0103f82a11410b91c8fb5752050a21"
}
],
"observability": {
"enabled": true
},
"upload_source_maps": true
}
Now update your typings by running npx wrangler types.
Connect to Hyperdrive
And now, via your same env object, you can connect to your database through Hyperdrive.
Note
If you havenât read Part 1 of this, you can grab the env object in a TanStack project with this special import
import { env } from "cloudflare:workers";
const pool = new Pool({
connectionString: env.HYPERDRIVE.connectionString,
});
But thereâs one more thing to do. When you attempt to run your app, youâll likely see this error

To fix this, add this connection string to your dev database with that key to your .env file.
CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE='postgresql://docker:docker@localhost:5432/your_db'
Fixing Per-Request Cleanup
Our database connections will be much snappier now. But Cloudflare will still error out since our database object is created and exported by a module. That means it will continue to live between requests.
Letâs see how to fix that.
What we need is a fresh database connection per request. And it turns out TanStack Start has a feature just for that: global request middleware
Note
Note that Iâm using the word âconnectionâ loosely here. Weâre creating a conceptual âconnectionâ to our database per request, but weâre really connecting through Hyperdrive, which pools and reuses actual database TCP connections between requests. The goal is not to literally create a fresh TCP connection to our database for each request; itâs to avoid long-lived I/O resources between requests.
The focus of this post is Cloudflare, so weâll breeze through the code; check the docs for more info.
import { Pool } from "pg";
import { createCsrfMiddleware, createMiddleware, createStart } from "@tanstack/react-start";
import { getDb } from "./data/db";
const globalContextMiddleware = createMiddleware().server(async ({ next }) => {
try {
const pool = new Pool({
connectionString: process.env.POSTGRES!,
});
const db = getDb(pool);
return next({
context: {
db,
},
});
} catch (error) {
console.log({ msg: "Error in root context middleware", error });
throw error;
}
});
const csrfMiddleware = createCsrfMiddleware({
filter: ctx => ctx.handlerType === "serverFn",
});
export const startInstance = createStart(() => ({
requestMiddleware: [csrfMiddleware, globalContextMiddleware],
functionMiddleware: [],
}));
This middleware will run once per request, which is exactly what we want. We create our database object and then add it to the context.
return next({
context: {
db,
},
});
Now you can access the db object from any server functions, or server routes (API routes) via the context object thatâs passed in.

Odds and Ends
If youâre connecting to a database thatâs hosted in a particular region, youâll almost always want your web app served from the same region. Putting the workers serving your app closer to your users might seem appealing, but that only serves to make the workers further from your database, increasing the latency of your queries and updates, and your app will likely need to make multiple requests to your database in the process of serving a request.
My PlanetScale database is in AWSâs us-east-1 region, and so I can pin my Cloudflare app to the same region with this entry in my Wrangler file.
"placement": {
"region": "aws:us-east-1",
},
It can make a large difference. I saw the latency in running a very simple query against a small table explode from about 7ms when placed in the same region as my database, up to over 10X (about 80ms) when placed on the United States West Coast.
Concluding Thoughts
I absolutely love Cloudflareâs development platform. Workers are an outstanding, low-latency way to host your web application. I hope this post has provided the tools you need to get your first app up and running there.
Article Series