From 4d2f84039609e95d65f53a9d1a527b69d5467a61 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 6 Feb 2026 17:45:01 +0000 Subject: [PATCH 1/3] Remove .end() calls, add handler notes Co-authored-by: elithrar --- .../concepts/connection-lifecycle.mdx | 13 +++-- .../hyperdrive/concepts/query-caching.mdx | 48 ++++++++++++------- .../drizzle-orm.mdx | 4 -- .../drizzle-orm.mdx | 4 -- .../prisma-orm.mdx | 4 -- src/content/docs/hyperdrive/get-started.mdx | 16 +------ src/content/docs/hyperdrive/index.mdx | 8 ---- ...rverless-timeseries-api-with-timescale.mdx | 7 --- .../hyperdrive/use-mysql-to-make-query.mdx | 2 - .../hyperdrive/use-mysql2-to-make-query.mdx | 5 -- .../use-node-postgres-to-make-query.mdx | 5 -- .../use-postgres-js-to-make-query.mdx | 5 -- 12 files changed, 38 insertions(+), 83 deletions(-) diff --git a/src/content/docs/hyperdrive/concepts/connection-lifecycle.mdx b/src/content/docs/hyperdrive/concepts/connection-lifecycle.mdx index 7f269ef20ba..3e9cb315b2d 100644 --- a/src/content/docs/hyperdrive/concepts/connection-lifecycle.mdx +++ b/src/content/docs/hyperdrive/concepts/connection-lifecycle.mdx @@ -41,7 +41,7 @@ In a Cloudflare Worker, database client connections within the Worker are only k When your Worker finishes processing a request, the database client is automatically garbage collected and the edge connection to Hyperdrive is cleaned up. Hyperdrive keeps the underlying connection to your origin database open in its pool for reuse. -While garbage collection handles cleanup, it is best practice to explicitly close your database client by calling `.end()` via [`ctx.waitUntil()`](/workers/runtime-apis/context/#waituntil). This ensures the client connection is closed cleanly after the response is sent, without blocking the response to the user: +You do **not** need to call `client.end()`, `sql.end()`, `connection.end()` (or similar) to clean up database clients. Workers-to-Hyperdrive connections are automatically cleaned up when the request or invocation ends, including when a [Workflow](/workflows/) or [Queue consumer](/queues/) completes, or when a [Durable Object](/durable-objects/) hibernates or is evicted when idle. ```ts import { Client } from "pg"; @@ -55,18 +55,17 @@ export default { const result = await client.query("SELECT * FROM pg_tables"); - // Signal Hyperdrive that this client is done. The underlying - // pooled connection to your database remains open for reuse. - ctx.waitUntil(client.end()); - + // No need to call client.end() — Hyperdrive automatically cleans + // up the client connection when the request ends. The underlying + // pooled connection to your origin database remains open for reuse. return Response.json(result.rows); }, } satisfies ExportedHandler; ``` -:::note +:::note[Create database clients inside your handlers] -Calling `.end()` on the client only closes the local client connection to Hyperdrive at the edge. It does **not** close the pooled connection to your origin database — Hyperdrive manages those connections and keeps them available for future requests. +You should always create database clients inside your request handlers (`fetch`, `queue`, and similar), not in the global scope. Workers do not allow [I/O across requests](/workers/runtime-apis/bindings/#making-changes-to-bindings), and Hyperdrive's distributed connection pooling already solves for connection startup latency. Using a driver-level pool (such as `new Pool()` or `createPool()`) in the global script scope will leave you with stale connections that result in failed queries and hard errors. ::: diff --git a/src/content/docs/hyperdrive/concepts/query-caching.mdx b/src/content/docs/hyperdrive/concepts/query-caching.mdx index d7a4adffdcc..9a85e87d7c2 100644 --- a/src/content/docs/hyperdrive/concepts/query-caching.mdx +++ b/src/content/docs/hyperdrive/concepts/query-caching.mdx @@ -88,29 +88,41 @@ For example, using database drivers: ```ts title="index.ts" -const client = postgres(env.HYPERDRIVE.connectionString); -// ... -const clientNoCache = postgres(env.HYPERDRIVE_CACHE_DISABLED.connectionString); +export default { + async fetch(request, env, ctx): Promise { + // Create clients inside your handler — not in global scope + const client = postgres(env.HYPERDRIVE.connectionString); + // ... + const clientNoCache = postgres(env.HYPERDRIVE_CACHE_DISABLED.connectionString); + // ... + }, +} satisfies ExportedHandler; ``` ```ts title="index.ts" -const connection = createConnection({ - host: env.HYPERDRIVE.host, - user: env.HYPERDRIVE.user, - password: env.HYPERDRIVE.password, - database: env.HYPERDRIVE.database, - port: env.HYPERDRIVE.port -}); -// ... -const connectionNoCache = createConnection({ - host: env.HYPERDRIVE_CACHE_DISABLED.host, - user: env.HYPERDRIVE_CACHE_DISABLED.user, - password: env.HYPERDRIVE_CACHE_DISABLED.password, - database: env.HYPERDRIVE_CACHE_DISABLED.database, - port: env.HYPERDRIVE_CACHE_DISABLED.port -}); +export default { + async fetch(request, env, ctx): Promise { + // Create connections inside your handler — not in global scope + const connection = await createConnection({ + host: env.HYPERDRIVE.host, + user: env.HYPERDRIVE.user, + password: env.HYPERDRIVE.password, + database: env.HYPERDRIVE.database, + port: env.HYPERDRIVE.port, + }); + // ... + const connectionNoCache = await createConnection({ + host: env.HYPERDRIVE_CACHE_DISABLED.host, + user: env.HYPERDRIVE_CACHE_DISABLED.user, + password: env.HYPERDRIVE_CACHE_DISABLED.password, + database: env.HYPERDRIVE_CACHE_DISABLED.database, + port: env.HYPERDRIVE_CACHE_DISABLED.port, + }); + // ... + }, +} satisfies ExportedHandler; ``` diff --git a/src/content/docs/hyperdrive/examples/connect-to-mysql/mysql-drivers-and-libraries/drizzle-orm.mdx b/src/content/docs/hyperdrive/examples/connect-to-mysql/mysql-drivers-and-libraries/drizzle-orm.mdx index 5e7647eb784..23a0882c5b1 100644 --- a/src/content/docs/hyperdrive/examples/connect-to-mysql/mysql-drivers-and-libraries/drizzle-orm.mdx +++ b/src/content/docs/hyperdrive/examples/connect-to-mysql/mysql-drivers-and-libraries/drizzle-orm.mdx @@ -93,10 +93,6 @@ export default { // Sample query to get all users const allUsers = await db.select().from(users); - // Clean up the client after the response is returned. - // Hyperdrive keeps the underlying connection open in its pool. - ctx.waitUntil(connection.end()); - return Response.json(allUsers); }, } satisfies ExportedHandler; diff --git a/src/content/docs/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/drizzle-orm.mdx b/src/content/docs/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/drizzle-orm.mdx index 54e23d8a8a5..0a02bf286fb 100644 --- a/src/content/docs/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/drizzle-orm.mdx +++ b/src/content/docs/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/drizzle-orm.mdx @@ -87,10 +87,6 @@ export default { // Sample query to get all users const allUsers = await db.select().from(users); - // Clean up the client after the response is returned. - // Hyperdrive keeps the underlying connection open in its pool. - ctx.waitUntil(client.end()); - return Response.json(allUsers); }, } satisfies ExportedHandler; diff --git a/src/content/docs/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/prisma-orm.mdx b/src/content/docs/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/prisma-orm.mdx index a8cb8bcba07..a435e3374dc 100644 --- a/src/content/docs/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/prisma-orm.mdx +++ b/src/content/docs/hyperdrive/examples/connect-to-postgres/postgres-drivers-and-libraries/prisma-orm.mdx @@ -136,10 +136,6 @@ export default { const allUsers = await prisma.user.findMany(); - // Clean up the Prisma client after the response is returned. - // Hyperdrive keeps the underlying connection open in its pool. - ctx.waitUntil(prisma.$disconnect()); - return Response.json({ newUser: user, allUsers: allUsers, diff --git a/src/content/docs/hyperdrive/get-started.mdx b/src/content/docs/hyperdrive/get-started.mdx index a3a450bb2b7..0788b033ff4 100644 --- a/src/content/docs/hyperdrive/get-started.mdx +++ b/src/content/docs/hyperdrive/get-started.mdx @@ -249,11 +249,6 @@ export default { // Sample query const results = await sql.query(`SELECT * FROM pg_tables`); - // Clean up the client after the response is returned. - // Hyperdrive will keep the underlying database connection open - // in its pool and reuse it for future requests. - ctx.waitUntil(sql.end()); - // Return result rows as JSON return Response.json(results.rows); } catch (e) { @@ -271,8 +266,7 @@ Upon receiving a request, the code above does the following: 1. Creates a new database client configured to connect to your database via Hyperdrive, using the Hyperdrive connection string. 2. Initiates a query via `await sql.query()` that outputs all tables (user and system created) in the database (as an example query). -3. Calls `ctx.waitUntil(sql.end())` to clean up the client connection after the response is returned. Hyperdrive keeps the underlying database connection open in its pool, so this only closes the local client — the pooled connection to your origin database is reused. -4. Returns the response as JSON to the client. +3. Returns the response as JSON to the client. Hyperdrive automatically cleans up the client connection when the request ends, and keeps the underlying database connection open in its pool for reuse. @@ -318,11 +312,6 @@ export default { 'SHOW tables;' ); - // Clean up the client after the response is returned. - // Hyperdrive will keep the underlying database connection open - // in its pool and reuse it for future requests. - ctx.waitUntil(connection.end()); - // Return result rows as JSON return new Response(JSON.stringify({ results, fields }), { headers: { @@ -348,8 +337,7 @@ Upon receiving a request, the code above does the following: 1. Creates a new database client configured to connect to your database via Hyperdrive, using the Hyperdrive connection string. 2. Initiates a query via `await connection.query` that outputs all tables (user and system created) in the database (as an example query). -3. Calls `ctx.waitUntil(connection.end())` to clean up the client connection after the response is returned. Hyperdrive keeps the underlying database connection open in its pool, so this only closes the local client — the pooled connection to your origin database is reused. -4. Returns the response as JSON to the client. +3. Returns the response as JSON to the client. Hyperdrive automatically cleans up the client connection when the request ends, and keeps the underlying database connection open in its pool for reuse. diff --git a/src/content/docs/hyperdrive/index.mdx b/src/content/docs/hyperdrive/index.mdx index 4349ec727a9..8b9e4a32122 100644 --- a/src/content/docs/hyperdrive/index.mdx +++ b/src/content/docs/hyperdrive/index.mdx @@ -58,10 +58,6 @@ export default { // Sample SQL query const result = await client.query("SELECT * FROM pg_tables"); - // Clean up the client after the response is returned. - // Hyperdrive keeps the underlying connection open in its pool. - ctx.waitUntil(client.end()); - return Response.json(result.rows); } catch (e) { return Response.json({ error: e instanceof Error ? e.message : e }, { status: 500 }); @@ -121,10 +117,6 @@ export default { const [results, fields] = await connection.query('SHOW tables;'); - // Clean up the client after the response is returned. - // Hyperdrive keeps the underlying connection open in its pool. - ctx.waitUntil(connection.end()); - return new Response(JSON.stringify({ results, fields }), { headers: { 'Content-Type': 'application/json', diff --git a/src/content/docs/hyperdrive/tutorials/serverless-timeseries-api-with-timescale.mdx b/src/content/docs/hyperdrive/tutorials/serverless-timeseries-api-with-timescale.mdx index 8c889f4e86c..b9f564b9020 100644 --- a/src/content/docs/hyperdrive/tutorials/serverless-timeseries-api-with-timescale.mdx +++ b/src/content/docs/hyperdrive/tutorials/serverless-timeseries-api-with-timescale.mdx @@ -208,10 +208,6 @@ export default { JSON.stringify(productData), ]); - // Clean up the client after the response is returned. - // Hyperdrive keeps the underlying connection open in its pool. - ctx.waitUntil(client.end()); - // Collect the raw row count inserted to return const resp = new Response(JSON.stringify(insertResult.rowCount), { headers: { "Content-Type": "application/json" }, @@ -229,9 +225,6 @@ export default { [limit], ); - // Clean up the client after the response is returned. - ctx.waitUntil(client.end()); - // Return the result as JSON const resp = new Response(JSON.stringify(result.rows), { headers: { "Content-Type": "application/json" }, diff --git a/src/content/partials/hyperdrive/use-mysql-to-make-query.mdx b/src/content/partials/hyperdrive/use-mysql-to-make-query.mdx index 1a696537f6d..673f080caa0 100644 --- a/src/content/partials/hyperdrive/use-mysql-to-make-query.mdx +++ b/src/content/partials/hyperdrive/use-mysql-to-make-query.mdx @@ -36,8 +36,6 @@ export default { // Sample query connection.query("SHOW tables;", [], (error, rows, fields) => { - connection.end(); - resolve({ fields, rows }); }); }); diff --git a/src/content/partials/hyperdrive/use-mysql2-to-make-query.mdx b/src/content/partials/hyperdrive/use-mysql2-to-make-query.mdx index 3928efa5b19..f2f8eb3b63e 100644 --- a/src/content/partials/hyperdrive/use-mysql2-to-make-query.mdx +++ b/src/content/partials/hyperdrive/use-mysql2-to-make-query.mdx @@ -41,11 +41,6 @@ export default { // Sample query const [results, fields] = await connection.query("SHOW tables;"); - // Clean up the client after the response is returned. - // Hyperdrive keeps the underlying database connection open in its - // pool and reuses it for future requests. - ctx.waitUntil(connection.end()); - // Return result rows as JSON return Response.json({ results, fields }); } catch (e) { diff --git a/src/content/partials/hyperdrive/use-node-postgres-to-make-query.mdx b/src/content/partials/hyperdrive/use-node-postgres-to-make-query.mdx index 17fb990851b..c8616777193 100644 --- a/src/content/partials/hyperdrive/use-node-postgres-to-make-query.mdx +++ b/src/content/partials/hyperdrive/use-node-postgres-to-make-query.mdx @@ -47,11 +47,6 @@ export default { // Perform a simple query const result = await client.query("SELECT * FROM pg_tables"); - // Clean up the client after the response is returned. - // Hyperdrive keeps the underlying database connection open in its - // pool and reuses it for future requests. - ctx.waitUntil(client.end()); - return Response.json({ success: true, result: result.rows, diff --git a/src/content/partials/hyperdrive/use-postgres-js-to-make-query.mdx b/src/content/partials/hyperdrive/use-postgres-js-to-make-query.mdx index a42fee106a8..b1275de22ae 100644 --- a/src/content/partials/hyperdrive/use-postgres-js-to-make-query.mdx +++ b/src/content/partials/hyperdrive/use-postgres-js-to-make-query.mdx @@ -45,11 +45,6 @@ export default { // A very simple test query const result = await sql`select * from pg_tables`; - // Clean up the client after the response is returned. - // Hyperdrive keeps the underlying database connection open in its - // pool and reuses it for future requests. - ctx.waitUntil(sql.end()); - // Return result rows as JSON return Response.json({ success: true, result: result }); } catch (e: any) { From d2534f67fb2d2b8b5ad2d13259c63df26405c15c Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 6 Feb 2026 17:54:24 +0000 Subject: [PATCH 2/3] Add stale connection and I/O context error strings to troubleshooting page Add exact error messages from node-postgres, postgres.js, mysql2, mysql, and the Workers runtime so users (and agents) can search for these strings and find the troubleshooting guidance. Covers errors thrown when database clients are created in global scope or reused across requests. --- .../observability/troubleshooting.mdx | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/content/docs/hyperdrive/observability/troubleshooting.mdx b/src/content/docs/hyperdrive/observability/troubleshooting.mdx index f9a56ce3a22..e88ef260e7f 100644 --- a/src/content/docs/hyperdrive/observability/troubleshooting.mdx +++ b/src/content/docs/hyperdrive/observability/troubleshooting.mdx @@ -68,6 +68,54 @@ If your queries are not getting cached despite Hyperdrive having caching enabled | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `Code generation from strings disallowed for this context` | The database driver you are using is attempting to use the `eval()` command, which is unsupported on Cloudflare Workers (common in `mysql2` driver). | Configure the database driver to not use `eval()`. See how to [configure `mysql2` to disable the usage of `eval()`](/hyperdrive/examples/connect-to-mysql/mysql-drivers-and-libraries/mysql2/). | +### Stale connection and I/O context errors + +These errors occur when a database client or connection is created in the global scope (outside of a request handler) or is reused across requests. Workers do not allow [I/O across requests](/workers/runtime-apis/bindings/#making-changes-to-bindings), and database connections from a previous request context become unusable. Always [create database clients inside your handlers](/hyperdrive/concepts/connection-lifecycle/#cleaning-up-client-connections). + +#### Workers runtime errors + +| Error Message | Details | Recommended fixes | +| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `Disallowed operation called within global scope. Asynchronous I/O (ex: fetch() or connect()), setting a timeout, and generating random values are not allowed within global scope.` | Your Worker is attempting to open a database connection or perform I/O during script startup, outside of a request handler. | Move the database client creation into your `fetch`, `queue`, or other handler function. | +| `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.` | A database connection or client created during one request is being reused in a subsequent request. | Create a new database client on every request instead of caching it in a global variable. Hyperdrive's connection pooling already eliminates the connection startup overhead. | + +#### node-postgres (`pg`) errors + +| Error Message | Details | Recommended fixes | +| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `Connection terminated` | The client's `.end()` method was called, or the connection was cleaned up at the end of a previous request. | Create a new `Client` inside your handler instead of reusing one from a prior request. | +| `Connection terminated unexpectedly` | The underlying connection was dropped without an explicit `.end()` call — for example, when a previous request's context was garbage collected. | Create a new `Client` inside your handler for every request. | +| `Client has encountered a connection error and is not queryable` | A socket-level error occurred on the connection (common when reusing a client across requests). | Create a new `Client` inside your handler. Do not store clients in global variables. | +| `Client was closed and is not queryable` | A query was attempted on a client whose `.end()` method was already called. | Create a new `Client` inside your handler instead of reusing one. | +| `Cannot use a pool after calling end on the pool` | `pool.connect()` was called on a `Pool` instance that has already been ended. | Do not use `new Pool()` in the global scope. Create a `new Client()` inside your handler — Hyperdrive handles connection pooling for you. | +| `Client has already been connected. You cannot reuse a client.` | `client.connect()` was called on a client that was already connected in a previous invocation. | Create a new `Client` per request. node-postgres clients cannot be reconnected once connected. | + +#### Postgres.js (`postgres`) errors + +Postgres.js error messages include the error code and the target host. The `code` property on the error object contains the error code. + +| Error Message | Details | Recommended fixes | +| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `write CONNECTION_ENDED :` | A query was attempted after `sql.end()` was called, or the connection was cleaned up from a prior request. Error code: `CONNECTION_ENDED`. | Create a new `postgres()` instance inside your handler. | +| `write CONNECTION_DESTROYED :` | The connection was forcefully terminated — for example, during `sql.end({ timeout })` expiration, or because the connection was already terminated. Error code: `CONNECTION_DESTROYED`. | Create a new `postgres()` instance inside your handler for every request. | +| `write CONNECTION_CLOSED :` | The underlying socket was closed unexpectedly while queries were still pending. Error code: `CONNECTION_CLOSED`. | Create a new `postgres()` instance inside your handler. If this occurs within a single request, check for network issues or query timeouts. | + +#### mysql2 errors + +| Error Message | Details | Recommended fixes | +| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `Can't add new command when connection is in closed state` | A query was attempted on a connection that has already been closed or encountered a fatal error. | Create a new connection inside your handler instead of reusing one from global scope. | +| `Connection lost: The server closed the connection.` | The underlying socket was closed by the server or was garbage collected between requests. Error code: `PROTOCOL_CONNECTION_LOST`. | Create a new connection inside your handler for every request. | +| `Pool is closed.` | `pool.getConnection()` was called on a pool that has already been closed. | Do not use `createPool()` in the global scope. Create a new `createConnection()` inside your handler — Hyperdrive handles pooling for you. | + +#### mysql errors + +| Error Message | Details | Recommended fixes | +| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `Cannot enqueue Query after fatal error.` | A query was attempted on a connection that previously encountered a fatal error. Error code: `PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR`. | Create a new connection inside your handler instead of reusing one from global scope. | +| `Cannot enqueue Query after invoking quit.` | A query was attempted on a connection after `.end()` was called. Error code: `PROTOCOL_ENQUEUE_AFTER_QUIT`. | Create a new connection inside your handler for every request. | +| `Cannot enqueue Handshake after already enqueuing a Handshake.` | `.connect()` was called on a connection that was already connected in a previous request. Error code: `PROTOCOL_ENQUEUE_HANDSHAKE_TWICE`. | Create a new connection per request. mysql connections cannot be reconnected once connected. | + ### Improve performance Having query traffic written as transactions can limit performance. This is because in the case of a transaction, the connection must be held for the duration of the transaction, which limits connection multiplexing. If there are multiple queries per transaction, this can be particularly impactful on connection multiplexing. Where possible, we recommend not wrapping queries in transactions to allow the connections to be shared more aggressively. From dee5722be5bd849aa61036102b8b9e1b74c48d15 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 6 Feb 2026 18:08:54 +0000 Subject: [PATCH 3/3] Add bad/good TypeScriptExample for global vs handler-scoped database clients --- .../concepts/connection-lifecycle.mdx | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/content/docs/hyperdrive/concepts/connection-lifecycle.mdx b/src/content/docs/hyperdrive/concepts/connection-lifecycle.mdx index 3e9cb315b2d..bd517175a32 100644 --- a/src/content/docs/hyperdrive/concepts/connection-lifecycle.mdx +++ b/src/content/docs/hyperdrive/concepts/connection-lifecycle.mdx @@ -5,6 +5,8 @@ sidebar: order: 2 --- +import { TypeScriptExample } from "~/components"; + Understanding how connections work between Workers, Hyperdrive, and your origin database is essential for building efficient applications with Hyperdrive. By maintaining a connection pool to your database within Cloudflare's network, Hyperdrive reduces seven round-trips to your database before you can even send a query: the TCP handshake (1x), TLS negotiation (3x), and database authentication (3x). @@ -69,6 +71,37 @@ You should always create database clients inside your request handlers (`fetch`, ::: +Do not create database clients or connection pools in the global scope. Instead, create a new client inside each handler invocation — Hyperdrive's connection pool ensures this is fast: + + +```ts +import { Client } from "pg"; + +// 🔴 Bad: Client created in the global scope persists across requests. +// Workers do not allow I/O across request contexts, so this client +// becomes stale and subsequent queries will throw hard errors. +const globalClient = new Client({ + connectionString: env.HYPERDRIVE.connectionString, +}); +await globalClient.connect(); + +export default { + async fetch(request, env, ctx): Promise { + // ✅ Good: Client created inside the handler, scoped to this request. + // Hyperdrive pools the underlying connection to your origin database, + // so creating a new client per request is fast and reliable. + const client = new Client({ + connectionString: env.HYPERDRIVE.connectionString, + }); + await client.connect(); + + const result = await client.query("SELECT * FROM pg_tables"); + return Response.json(result.rows); + }, +} satisfies ExportedHandler; +``` + + ## Connection lifecycle considerations ### Durable Objects and persistent connections