Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:24:16 +08:00
commit cfc6c441b5
22 changed files with 5629 additions and 0 deletions

View 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/)