Skip to content

Connection Pooling ​

Connection pooling is critical for production applications. Effect SQL leverages the connection pooling capabilities of the underlying database drivers.

Why Connection Pooling? ​

Creating database connections is expensive:

  • TCP handshake
  • SSL negotiation
  • Authentication
  • Database session setup

Connection pools:

  • Reuse existing connections
  • Limit maximum connections
  • Handle connection lifecycle
  • Provide backpressure when overloaded

Pool Configuration ​

PostgreSQL ​

typescript
import { PgClient } from "@effect/sql-pg"
import { Duration } from "effect"

const DatabaseLive = PgClient.layer({
  host: "localhost",
  database: "myapp",
  
  // Pool size
  minConnections: 5,        // Minimum idle connections
  maxConnections: 20,       // Maximum pool size
  
  // Connection lifecycle
  idleTimeout: Duration.minutes(10),    // Close idle connections after 10 min
  connectionTTL: Duration.minutes(30),  // Recycle connections after 30 min
  connectTimeout: Duration.seconds(10)  // Fail if can't connect in 10s
})

MySQL ​

typescript
import { MysqlClient } from "@effect/sql-mysql2"

const DatabaseLive = MysqlClient.layer({
  host: "localhost",
  database: "myapp",
  
  minConnections: 5,
  maxConnections: 20,
  connectTimeout: Duration.seconds(10)
})

SQLite ​

SQLite uses a single connection with a semaphore for concurrency control:

typescript
import { SqliteClient } from "@effect/sql-sqlite-node"

const DatabaseLive = SqliteClient.layer({
  filename: "./app.db"
  // SQLite doesn't use traditional pooling
  // Effect SQL serializes writes automatically
})

Sizing Guidelines ​

General Formula ​

Connections = (CPU cores * 2) + number of disks

For cloud databases without direct disk access, start with:

  • Small: 5-10 connections
  • Medium: 10-20 connections
  • Large: 20-50 connections

Factors to Consider ​

  1. Available connections on database server

    • PostgreSQL default: 100
    • MySQL default: 151
    • Each app instance needs its share
  2. Application concurrency

    • How many concurrent requests?
    • Are queries long-running?
  3. Transaction duration

    • Long transactions hold connections
  4. Multiple services

    • Total connections across all services must fit limit

Monitoring ​

Check Current Pool Status ​

typescript
// PostgreSQL: Check active connections
const stats = yield* sql`
  SELECT 
    count(*) as total,
    count(*) FILTER (WHERE state = 'active') as active,
    count(*) FILTER (WHERE state = 'idle') as idle
  FROM pg_stat_activity 
  WHERE datname = current_database()
`

Connection Wait Time ​

If queries are slow but the database is fast, you might be waiting for connections:

typescript
// Add timing around operations
const startTime = Date.now()
const result = yield* sql`SELECT 1`
const duration = Date.now() - startTime
// If duration >> query time, you're waiting for connections

Production Patterns ​

Graceful Shutdown ​

typescript
import { Effect, Runtime, Layer } from "effect"

// Layer handles cleanup automatically
const program = Effect.gen(function* () {
  yield* startServer
  yield* Effect.never  // Keep running
})

// Cleanup happens when the runtime exits
Effect.runPromise(program.pipe(
  Effect.provide(DatabaseLive),
  Effect.scoped
))

Health Checks ​

typescript
const healthCheck = Effect.gen(function* () {
  const sql = yield* SqlClient.SqlClient
  
  // Simple connectivity check
  const result = yield* sql`SELECT 1 as ok`.pipe(
    Effect.timeout("5 seconds"),
    Effect.either
  )
  
  return Either.isRight(result)
})

Connection Validation ​

Some pools validate connections before use:

typescript
const DatabaseLive = PgClient.layer({
  // ... other options
  
  // The pg driver validates connections automatically
  // Stale connections are replaced
})

Common Issues ​

Connection Exhaustion ​

Symptom: Queries hang, timeouts increase

Causes:

  • Pool too small
  • Long-running transactions
  • Connection leaks (not using Effect properly)

Solutions:

typescript
// Increase pool size
maxConnections: 30

// Add query timeouts
yield* sql`SELECT ...`.pipe(
  Effect.timeout("10 seconds")
)

// Always use Effect.scoped for reserved connections
yield* Effect.scoped(
  Effect.gen(function* () {
    const conn = yield* sql.reserve
    // Connection released when scope exits
  })
)

Connection Thrashing ​

Symptom: High connection churn, performance degradation

Causes:

  • Min connections too low
  • Idle timeout too short

Solutions:

typescript
minConnections: 5,           // Keep some connections warm
idleTimeout: Duration.minutes(10)  // Don't kill idle connections too fast

Stale Connections ​

Symptom: Intermittent query failures

Causes:

  • Network issues
  • Database restarts
  • Firewall timeouts

Solutions:

typescript
connectionTTL: Duration.minutes(30),  // Recycle connections periodically
// Retry transient errors
yield* sql`SELECT ...`.pipe(
  Effect.retry({ times: 3, delay: "100 millis" })
)

Database-Specific Notes ​

PostgreSQL ​

  • Uses pg-pool
  • Supports prepared statement caching per connection
  • LISTEN/NOTIFY requires dedicated connection

MySQL ​

  • Uses mysql2 pool
  • Binary protocol for better performance
  • Connection-level charset settings

SQLite ​

  • Single connection, no pooling
  • WAL mode enables concurrent reads
  • Writes are serialized by Effect SQL

Cloud Databases ​

PgBouncer / Connection Proxies ​

When using a connection pooler like PgBouncer:

typescript
const DatabaseLive = PgClient.layer({
  host: "pgbouncer.example.com",
  
  // Use session mode for transactions
  // Transaction pooling breaks named prepared statements
  
  // Smaller pool - the pooler manages connections
  maxConnections: 10
})

Serverless (Neon, PlanetScale) ​

typescript
// Serverless databases handle pooling
// Use minimal local pool
const DatabaseLive = PgClient.layer({
  host: "your-db.neon.tech",
  maxConnections: 5,
  ssl: true
})

Next Steps ​

Released under the MIT License.