Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 39 additions & 7 deletions src/content/docs/hyperdrive/concepts/connection-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -41,7 +43,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";
Expand All @@ -55,21 +57,51 @@ 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<Env>;
```

:::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.
Comment thread
elithrar marked this conversation as resolved.
Comment thread
elithrar marked this conversation as resolved.

:::

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:

<TypeScriptExample filename="index.ts">
```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<Response> {
// ✅ 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<Env>;
```
</TypeScriptExample>

## Connection lifecycle considerations

### Durable Objects and persistent connections
Expand Down
48 changes: 30 additions & 18 deletions src/content/docs/hyperdrive/concepts/query-caching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,29 +88,41 @@ For example, using database drivers:
<Tabs>
<TabItem label="PostgreSQL">
```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<Response> {
// 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<Env>;
```
</TabItem>

<TabItem label="MySQL">
```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<Response> {
// 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<Env>;
```
</TabItem>
</Tabs>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Env>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Env>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 2 additions & 14 deletions src/content/docs/hyperdrive/get-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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.

</TabItem>
<TabItem label="MySQL">
Expand Down Expand Up @@ -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: {
Expand All @@ -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.

</TabItem>
</Tabs>
Expand Down
8 changes: 0 additions & 8 deletions src/content/docs/hyperdrive/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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',
Expand Down
Loading
Loading