Topic Overview

Connection Pooling

Understand connection pooling to reuse database and network connections efficiently, reducing overhead and improving performance.


What is Connection Pooling?

Connection pooling maintains a pool of connections:

  • Reuse connections: Don't create new connection for each request
  • Reduce overhead: Avoid connection establishment cost
  • Limit connections: Control number of concurrent connections
  • Improve performance: Faster requests (no connection setup)

Why needed:

  • Connection overhead: Creating connections is expensive
  • Resource limits: Databases have connection limits
  • Performance: Reusing connections is faster

How Connection Pooling Works

Without Pooling

Request 1: Create connection → Use → Close
Request 2: Create connection → Use → Close
Request 3: Create connection → Use → Close

Overhead: Connection setup/teardown for each request

With Pooling

Pool: [Connection1, Connection2, Connection3]

Request 1: Get connection from pool → Use → Return to pool
Request 2: Get connection from pool → Use → Return to pool
Request 3: Get connection from pool → Use → Return to pool

Benefits: Reuse connections, reduce overhead


Connection Pool Lifecycle

1. Initialize pool (create connections)
2. Request: Get connection from pool
3. Use connection
4. Return connection to pool
5. Cleanup: Close idle connections

Examples

Database Connection Pool

import psycopg2
from psycopg2 import pool

class DatabaseConnectionPool:
    def __init__(self, min_connections=2, max_connections=10):
        self.pool = psycopg2.pool.SimpleConnectionPool(
            min_connections,
            max_connections,
            host='localhost',
            database='mydb',
            user='user',
            password='password'
        )
    
    def get_connection(self):
        """Get connection from pool"""
        return self.pool.getconn()
    
    def return_connection(self, conn):
        """Return connection to pool"""
        self.pool.putconn(conn)
    
    def execute_query(self, query):
        """Execute query using pooled connection"""
        conn = self.get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute(query)
            result = cursor.fetchall()
            return result
        finally:
            self.return_connection(conn)

HTTP Connection Pool

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

class HTTPConnectionPool:
    def __init__(self):
        self.session = requests.Session()
        
        # Configure connection pool
        adapter = HTTPAdapter(
            pool_connections=10,  # Number of connection pools
            pool_maxsize=20,      # Max connections per pool
            max_retries=Retry(total=3)
        )
        
        self.session.mount('http://', adapter)
        self.session.mount('https://', adapter)
    
    def get(self, url):
        """GET request using pooled connection"""
        return self.session.get(url)
    
    def post(self, url, data):
        """POST request using pooled connection"""
        return self.session.post(url, data)

Custom Connection Pool

import threading
import queue

class ConnectionPool:
    def __init__(self, factory, max_size=10, min_size=2):
        self.factory = factory  # Function to create connection
        self.max_size = max_size
        self.min_size = min_size
        self.pool = queue.Queue(maxsize=max_size)
        self.lock = threading.Lock()
        self.active = 0
        
        # Initialize pool
        for _ in range(min_size):
            conn = self.factory()
            self.pool.put(conn)
    
    def get_connection(self, timeout=5):
        """Get connection from pool"""
        try:
            # Get from pool (non-blocking)
            conn = self.pool.get(timeout=timeout)
            self.active += 1
            return conn
        except queue.Empty:
            # Pool empty: Create new if under max
            if self.active < self.max_size:
                conn = self.factory()
                self.active += 1
                return conn
            else:
                raise PoolExhaustedError("Connection pool exhausted")
    
    def return_connection(self, conn):
        """Return connection to pool"""
        # Check if connection is still valid
        if self.is_valid(conn):
            self.pool.put(conn)
        else:
            # Create new connection
            new_conn = self.factory()
            self.pool.put(new_conn)
        
        self.active -= 1
    
    def is_valid(self, conn):
        """Check if connection is still valid"""
        try:
            # Ping connection
            conn.ping()
            return True
        except:
            return False

Common Pitfalls

  • Pool exhaustion: Too many connections. Fix: Set appropriate max size, timeout
  • Stale connections: Connections become invalid. Fix: Validate connections, recreate if needed
  • Connection leaks: Connections not returned. Fix: Use try-finally, connection managers
  • Too small pool: Not enough connections. Fix: Size pool based on load

Interview Questions

Beginner

Q: What is connection pooling and why is it used?

A:

Connection pooling maintains a pool of reusable connections.

Why used:

  • Reduce overhead: Avoid creating connections for each request
  • Improve performance: Reusing connections is faster
  • Resource limits: Databases have connection limits
  • Efficiency: Better resource utilization

How it works:

Without pooling:
  Request → Create connection → Use → Close
  (Overhead: Connection setup/teardown)

With pooling:
  Pool: [Connection1, Connection2, Connection3]
  Request → Get from pool → Use → Return to pool
  (Reuse connections, reduce overhead)

Benefits:

  • Faster: No connection setup per request
  • Efficient: Reuse connections
  • Controlled: Limit number of connections

Intermediate

Q: Explain how connection pooling works. How do you handle connection validation and pool sizing?

A:

Connection Pool Lifecycle:

  1. Initialize pool

    # Create initial connections
    pool = [conn1, conn2, conn3]
    
  2. Get connection

    conn = pool.get_connection()
    
  3. Use connection

    result = conn.execute(query)
    
  4. Return connection

    pool.return_connection(conn)
    

Connection Validation:

def return_connection(conn):
    # Validate before returning
    if conn.is_valid():
        pool.put(conn)
    else:
        # Create new connection
        new_conn = create_connection()
        pool.put(new_conn)

Pool Sizing:

# Size based on:
max_connections = (
    expected_concurrent_requests * 
    average_request_duration / 
    target_response_time
)

# Example:
# 100 concurrent requests
# 100ms average duration
# Target: 1s response time
# Max connections: 100 * 0.1 / 1 = 10

Best practices:

  • Min size: Keep minimum connections ready
  • Max size: Limit to prevent exhaustion
  • Validation: Check connections before reuse
  • Timeout: Timeout when pool exhausted

Senior

Q: Design a connection pooling system for a high-traffic application. How do you handle pool sizing, connection health checks, and prevent connection leaks?

A:

class HighPerformanceConnectionPool {
  private pool: Connection[];
  private available: Queue<Connection>;
  private inUse: Set<Connection>;
  private healthChecker: HealthChecker;
  
  constructor(config: PoolConfig) {
    this.pool = [];
    this.available = new Queue();
    this.inUse = new Set();
    this.healthChecker = new HealthChecker();
    
    // Initialize pool
    this.initializePool(config.minSize);
  }
  
  // 1. Pool Management
  async getConnection(timeout: number = 5000): Promise<Connection> {
    // Try to get from available
    let conn = this.available.dequeue();
    
    if (conn) {
      // Validate connection
      if (await this.healthChecker.isHealthy(conn)) {
        this.inUse.add(conn);
        return conn;
      } else {
        // Invalid: Create new
        conn = await this.createConnection();
      }
    } else {
      // Pool empty: Create new if under max
      if (this.pool.length < this.maxSize) {
        conn = await this.createConnection();
        this.pool.push(conn);
      } else {
        // Wait for available connection
        conn = await this.waitForConnection(timeout);
      }
    }
    
    this.inUse.add(conn);
    return conn;
  }
  
  async returnConnection(conn: Connection): Promise<void> {
    this.inUse.delete(conn);
    
    // Validate before returning
    if (await this.healthChecker.isHealthy(conn)) {
      this.available.enqueue(conn);
    } else {
      // Remove invalid connection
      this.removeConnection(conn);
      // Create replacement
      const newConn = await this.createConnection();
      this.pool.push(newConn);
      this.available.enqueue(newConn);
    }
  }
  
  // 2. Health Checking
  class HealthChecker {
    async isHealthy(conn: Connection): Promise<boolean> {
      try {
        // Ping connection
        await conn.ping();
        return true;
      } catch {
        return false;
      }
    }
    
    async checkAll(): Promise<void> {
      // Periodically check all connections
      for (const conn of this.pool) {
        if (!await this.isHealthy(conn)) {
          await this.replaceConnection(conn);
        }
      }
    }
  }
  
  // 3. Leak Detection
  class LeakDetector {
    detectLeaks(): void {
      // Check for connections in use too long
      for (const conn of this.inUse) {
        const age = Date.now() - conn.acquiredAt;
        if (age > 60000) { // 1 minute
          // Potential leak
          this.alert(`Connection ${conn.id} in use for ${age}ms`);
        }
      }
    }
  }
  
  // 4. Dynamic Sizing
  class DynamicPoolSizer {
    async adjustSize(): Promise<void> {
      const utilization = this.inUse.size / this.pool.length;
      
      if (utilization > 0.8) {
        // High utilization: Increase pool
        await this.increasePool();
      } else if (utilization < 0.3) {
        // Low utilization: Decrease pool
        await this.decreasePool();
      }
    }
  }
}

Features:

  1. Pool management: Get, return, validate connections
  2. Health checking: Validate connections before reuse
  3. Leak detection: Detect connections in use too long
  4. Dynamic sizing: Adjust pool size based on utilization

Key Takeaways

  • Connection pooling: Maintain pool of reusable connections
  • Benefits: Reduce overhead, improve performance, control resources
  • Lifecycle: Initialize → Get → Use → Return → Cleanup
  • Validation: Check connections before reuse
  • Pool sizing: Based on load, concurrent requests, response time
  • Best practices: Validate connections, detect leaks, size appropriately

About the author

InterviewCrafted helps you master system design with patience. We believe in curiosity-led engineering, reflective writing, and designing systems that make future changes feel calm.