Initial commit
This commit is contained in:
521
references/connection-pooling.md
Normal file
521
references/connection-pooling.md
Normal file
@@ -0,0 +1,521 @@
|
||||
# Connection Pooling Guide
|
||||
|
||||
Complete guide to connection pooling patterns with Cloudflare Hyperdrive.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
**Why Pooling Matters**:
|
||||
- Workers have a limit of **6 concurrent external connections**
|
||||
- Database drivers create new connections for each operation
|
||||
- Connection pooling reuses connections across queries
|
||||
- Hyperdrive provides global connection pooling near your database
|
||||
|
||||
---
|
||||
|
||||
## Workers Connection Limit
|
||||
|
||||
### The 6 Connection Rule
|
||||
|
||||
Cloudflare Workers can maintain **maximum 6 concurrent external TCP connections** (includes database connections).
|
||||
|
||||
**What counts toward limit**:
|
||||
- ✅ Database connections (pg.Client, pg.Pool, mysql2)
|
||||
- ✅ HTTP requests to external APIs
|
||||
- ✅ WebSocket connections
|
||||
- ✅ Any TCP socket connection
|
||||
|
||||
**What doesn't count**:
|
||||
- ❌ Requests to Cloudflare services (D1, KV, R2, Workers AI)
|
||||
- ❌ Requests within Cloudflare network
|
||||
- ❌ HTTP requests to other Workers
|
||||
|
||||
**Source**: [Workers Platform Limits](https://developers.cloudflare.com/workers/platform/limits)
|
||||
|
||||
---
|
||||
|
||||
## Connection Pool Sizing
|
||||
|
||||
### Rule of Thumb
|
||||
|
||||
**Set `max: 5` for connection pools** to stay within Workers' 6 connection limit.
|
||||
|
||||
```typescript
|
||||
// PostgreSQL (node-postgres)
|
||||
const pool = new Pool({
|
||||
connectionString: env.HYPERDRIVE.connectionString,
|
||||
max: 5 // Leave 1 connection for other operations
|
||||
});
|
||||
|
||||
// PostgreSQL (postgres.js)
|
||||
const sql = postgres(env.HYPERDRIVE.connectionString, {
|
||||
max: 5
|
||||
});
|
||||
```
|
||||
|
||||
### Why Not 6?
|
||||
|
||||
- Reserve 1 connection for other operations
|
||||
- Prevents hitting connection limit errors
|
||||
- Allows concurrent database + API requests
|
||||
- Safety margin for connection acquisition
|
||||
|
||||
---
|
||||
|
||||
## Connection Patterns
|
||||
|
||||
### Pattern 1: Single Connection (pg.Client)
|
||||
|
||||
**Use Case**: Simple queries, one query per request
|
||||
|
||||
```typescript
|
||||
import { Client } from "pg";
|
||||
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
const client = new Client({
|
||||
connectionString: env.HYPERDRIVE.connectionString
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
const result = await client.query('SELECT * FROM users');
|
||||
return Response.json(result.rows);
|
||||
} finally {
|
||||
ctx.waitUntil(client.end());
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Pros**:
|
||||
- Simple, straightforward
|
||||
- One connection per request
|
||||
- Easy to reason about
|
||||
|
||||
**Cons**:
|
||||
- Can't run parallel queries
|
||||
- Creates new connection for each request (mitigated by Hyperdrive)
|
||||
|
||||
**When to use**:
|
||||
- Single query per request
|
||||
- No parallel operations needed
|
||||
- Simple CRUD operations
|
||||
|
||||
---
|
||||
|
||||
### Pattern 2: Connection Pool (pg.Pool)
|
||||
|
||||
**Use Case**: Multiple parallel queries in single request
|
||||
|
||||
```typescript
|
||||
import { Pool } from "pg";
|
||||
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
const pool = new Pool({
|
||||
connectionString: env.HYPERDRIVE.connectionString,
|
||||
max: 5,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 10000
|
||||
});
|
||||
|
||||
try {
|
||||
// Run parallel queries
|
||||
const [users, posts, stats] = await Promise.all([
|
||||
pool.query('SELECT * FROM users'),
|
||||
pool.query('SELECT * FROM posts'),
|
||||
pool.query('SELECT COUNT(*) FROM comments')
|
||||
]);
|
||||
|
||||
return Response.json({ users: users.rows, posts: posts.rows, stats: stats.rows });
|
||||
} finally {
|
||||
ctx.waitUntil(pool.end());
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Pros**:
|
||||
- Parallel queries possible
|
||||
- Better performance for multiple operations
|
||||
- Efficient connection reuse
|
||||
|
||||
**Cons**:
|
||||
- More complex
|
||||
- Must manage pool lifecycle
|
||||
- Must set max correctly
|
||||
|
||||
**When to use**:
|
||||
- Multiple queries per request
|
||||
- Parallel operations needed
|
||||
- Complex data fetching
|
||||
|
||||
---
|
||||
|
||||
### Pattern 3: Reusable Pool (Advanced)
|
||||
|
||||
**⚠️ Advanced Pattern**: Create pool once, reuse across requests.
|
||||
|
||||
```typescript
|
||||
import { Pool } from "pg";
|
||||
|
||||
// Create pool outside handler (reused across requests)
|
||||
let pool: Pool | null = null;
|
||||
|
||||
function getPool(connectionString: string): Pool {
|
||||
if (!pool) {
|
||||
pool = new Pool({
|
||||
connectionString,
|
||||
max: 5,
|
||||
idleTimeoutMillis: 60000
|
||||
});
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
const pool = getPool(env.HYPERDRIVE.connectionString);
|
||||
|
||||
const result = await pool.query('SELECT * FROM users');
|
||||
return Response.json(result.rows);
|
||||
|
||||
// NOTE: Don't call pool.end() here - pool is reused
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Pros**:
|
||||
- Maximum connection reuse
|
||||
- Best performance
|
||||
- No connection overhead
|
||||
|
||||
**Cons**:
|
||||
- Global state (not recommended for Workers)
|
||||
- Harder to debug
|
||||
- Connection lifecycle management complex
|
||||
|
||||
**When to use**:
|
||||
- High-traffic applications
|
||||
- Performance-critical paths
|
||||
- When you understand Workers execution model well
|
||||
|
||||
**⚠️ Caution**: Workers may be restarted, pool will be recreated. This pattern works but requires careful testing.
|
||||
|
||||
---
|
||||
|
||||
## Connection Pool Configuration
|
||||
|
||||
### pg.Pool Options
|
||||
|
||||
```typescript
|
||||
const pool = new Pool({
|
||||
// Connection string (Hyperdrive)
|
||||
connectionString: env.HYPERDRIVE.connectionString,
|
||||
|
||||
// Maximum connections in pool (CRITICAL: set to 5)
|
||||
max: 5,
|
||||
|
||||
// Close idle connections after 30 seconds
|
||||
idleTimeoutMillis: 30000,
|
||||
|
||||
// Timeout when acquiring connection (10 seconds)
|
||||
connectionTimeoutMillis: 10000,
|
||||
|
||||
// Allow exiting process if no connections active
|
||||
allowExitOnIdle: false
|
||||
});
|
||||
```
|
||||
|
||||
### postgres.js Options
|
||||
|
||||
```typescript
|
||||
const sql = postgres(env.HYPERDRIVE.connectionString, {
|
||||
// Maximum connections
|
||||
max: 5,
|
||||
|
||||
// Idle connection timeout (seconds)
|
||||
idle_timeout: 30,
|
||||
|
||||
// Connection timeout (seconds)
|
||||
connect_timeout: 10,
|
||||
|
||||
// Prepared statements (CRITICAL for caching)
|
||||
prepare: true
|
||||
});
|
||||
```
|
||||
|
||||
### mysql2 Options
|
||||
|
||||
```typescript
|
||||
const pool = mysql.createPool({
|
||||
host: env.HYPERDRIVE.host,
|
||||
user: env.HYPERDRIVE.user,
|
||||
password: env.HYPERDRIVE.password,
|
||||
database: env.HYPERDRIVE.database,
|
||||
port: env.HYPERDRIVE.port,
|
||||
|
||||
// Maximum connections
|
||||
connectionLimit: 5,
|
||||
|
||||
// Queue if pool exhausted
|
||||
queueLimit: 0,
|
||||
|
||||
// Required for Workers
|
||||
disableEval: true
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Connection Cleanup
|
||||
|
||||
### The ctx.waitUntil() Pattern
|
||||
|
||||
**CRITICAL**: Always use `ctx.waitUntil()` to clean up connections AFTER response is sent.
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
const client = new Client({ connectionString: env.HYPERDRIVE.connectionString });
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
const result = await client.query('SELECT ...');
|
||||
return Response.json(result.rows); // Response sent here
|
||||
} finally {
|
||||
// Connection closed AFTER response sent (non-blocking)
|
||||
ctx.waitUntil(client.end());
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Why**:
|
||||
- Allows Worker to return response immediately
|
||||
- Connection cleanup happens in background
|
||||
- Prevents adding latency to response time
|
||||
- Prevents connection leaks
|
||||
|
||||
**DON'T do this**:
|
||||
```typescript
|
||||
await client.end(); // ❌ Blocks response, adds latency
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Transaction Management
|
||||
|
||||
### Transactions and Connection Pooling
|
||||
|
||||
**Problem**: Transactions hold connections for their duration, limiting pool availability.
|
||||
|
||||
**Impact on Pooling**:
|
||||
```typescript
|
||||
// ❌ Bad: Long transaction holds connection
|
||||
const client = await pool.connect();
|
||||
await client.query('BEGIN');
|
||||
// ... many queries (connection held) ...
|
||||
await client.query('COMMIT');
|
||||
client.release();
|
||||
```
|
||||
|
||||
**Better Approach**:
|
||||
```typescript
|
||||
// ✅ Good: Short transactions
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await client.query('INSERT ...');
|
||||
await client.query('UPDATE ...');
|
||||
await client.query('COMMIT');
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release(); // Return connection to pool quickly
|
||||
}
|
||||
```
|
||||
|
||||
**Best Practice**:
|
||||
- Keep transactions as short as possible
|
||||
- Avoid holding connections during I/O operations
|
||||
- Release connections back to pool immediately after transaction
|
||||
|
||||
---
|
||||
|
||||
## Monitoring Connection Usage
|
||||
|
||||
### Check Active Connections
|
||||
|
||||
```typescript
|
||||
// pg.Pool
|
||||
console.log('Total clients:', pool.totalCount);
|
||||
console.log('Idle clients:', pool.idleCount);
|
||||
console.log('Waiting clients:', pool.waitingCount);
|
||||
|
||||
// postgres.js
|
||||
// No built-in monitoring (check database side)
|
||||
```
|
||||
|
||||
### Database-Side Monitoring
|
||||
|
||||
**PostgreSQL**:
|
||||
```sql
|
||||
SELECT count(*) FROM pg_stat_activity WHERE datname = 'mydb';
|
||||
```
|
||||
|
||||
**MySQL**:
|
||||
```sql
|
||||
SHOW STATUS LIKE 'Threads_connected';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Connection Pool Exhaustion
|
||||
|
||||
### Symptoms
|
||||
|
||||
**Error**: `Failed to acquire a connection from the pool`
|
||||
|
||||
**Causes**:
|
||||
1. Too many concurrent requests
|
||||
2. Long-running queries holding connections
|
||||
3. Transactions not releasing connections
|
||||
4. Connection leaks (not calling `client.end()`)
|
||||
|
||||
### Solutions
|
||||
|
||||
**1. Use ctx.waitUntil() for cleanup**:
|
||||
```typescript
|
||||
ctx.waitUntil(client.end());
|
||||
```
|
||||
|
||||
**2. Set connection timeouts**:
|
||||
```typescript
|
||||
const pool = new Pool({
|
||||
max: 5,
|
||||
connectionTimeoutMillis: 10000 // Fail fast if can't acquire
|
||||
});
|
||||
```
|
||||
|
||||
**3. Keep transactions short**:
|
||||
```typescript
|
||||
// Minimize time between BEGIN and COMMIT
|
||||
```
|
||||
|
||||
**4. Monitor pool metrics**:
|
||||
```typescript
|
||||
console.log('Pool stats:', {
|
||||
total: pool.totalCount,
|
||||
idle: pool.idleCount,
|
||||
waiting: pool.waitingCount
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hyperdrive's Global Pooling
|
||||
|
||||
### How Hyperdrive Helps
|
||||
|
||||
**Without Hyperdrive**:
|
||||
- Each Worker request creates new connection
|
||||
- 7 round trips for each connection (TCP + TLS + auth)
|
||||
- High latency, many connections to database
|
||||
|
||||
**With Hyperdrive**:
|
||||
- Global connection pool near your database
|
||||
- Connection reused across Workers globally
|
||||
- Only 1 round trip (Worker to Hyperdrive edge)
|
||||
- Reduced connections to database
|
||||
|
||||
**Result**:
|
||||
- Lower latency
|
||||
- Fewer connections to database
|
||||
- Better scalability
|
||||
|
||||
### Hyperdrive + Local Pooling
|
||||
|
||||
**Best of both worlds**:
|
||||
```typescript
|
||||
// Local pool (max: 5) for parallel queries in single request
|
||||
const pool = new Pool({ max: 5, connectionString: env.HYPERDRIVE.connectionString });
|
||||
|
||||
// Hyperdrive's global pool handles reuse across Workers globally
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Parallel queries within request (local pool)
|
||||
- Connection reuse across Workers (Hyperdrive)
|
||||
- Optimal performance
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always set max: 5** for connection pools
|
||||
2. **Use ctx.waitUntil()** for connection cleanup
|
||||
3. **Keep transactions short** to free connections quickly
|
||||
4. **Use connection pools** for parallel queries, single connections otherwise
|
||||
5. **Monitor connection usage** in development
|
||||
6. **Set connection timeouts** to fail fast
|
||||
7. **Release connections immediately** after use
|
||||
8. **Avoid holding connections** during I/O operations
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
❌ **Mistake 1**: Setting max > 5
|
||||
```typescript
|
||||
const pool = new Pool({ max: 10 }); // Exceeds Workers' limit
|
||||
```
|
||||
|
||||
❌ **Mistake 2**: Not cleaning up connections
|
||||
```typescript
|
||||
const client = new Client(...);
|
||||
await client.connect();
|
||||
const result = await client.query('SELECT ...');
|
||||
return Response.json(result.rows);
|
||||
// Connection leak! No client.end()
|
||||
```
|
||||
|
||||
❌ **Mistake 3**: Blocking response with cleanup
|
||||
```typescript
|
||||
await client.end(); // Adds latency to response
|
||||
```
|
||||
|
||||
❌ **Mistake 4**: Long transactions
|
||||
```typescript
|
||||
await client.query('BEGIN');
|
||||
await fetch('https://api.example.com'); // Holding connection during HTTP request!
|
||||
await client.query('COMMIT');
|
||||
```
|
||||
|
||||
✅ **Correct Pattern**:
|
||||
```typescript
|
||||
const pool = new Pool({ max: 5, connectionString: env.HYPERDRIVE.connectionString });
|
||||
|
||||
try {
|
||||
const [users, posts] = await Promise.all([
|
||||
pool.query('SELECT * FROM users'),
|
||||
pool.query('SELECT * FROM posts')
|
||||
]);
|
||||
return Response.json({ users: users.rows, posts: posts.rows });
|
||||
} finally {
|
||||
ctx.waitUntil(pool.end());
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Workers Platform Limits](https://developers.cloudflare.com/workers/platform/limits)
|
||||
- [node-postgres Pooling](https://node-postgres.com/apis/pool)
|
||||
- [postgres.js Connection Options](https://github.com/porsager/postgres)
|
||||
- [How Hyperdrive Works](https://developers.cloudflare.com/hyperdrive/configuration/how-hyperdrive-works/)
|
||||
Reference in New Issue
Block a user