Connection Pooling
Django-CFG provides connection pooling for gRPC channels, reducing connection overhead and improving performance for high-traffic applications.
Why Connection Pooling?
Creating a new gRPC channel for each request is expensive:
| Operation | Without Pool | With Pool |
|---|---|---|
| Connection setup | Every request | Once per target |
| TLS handshake | Every request | Once per target |
| Channel warmup | Every request | Once per target |
| Typical latency | 50-200ms | 1-5ms |
Connection pooling reuses channels across requests, dramatically reducing latency.
Quick Start
from django_cfg.modules.django_grpc.services.client import (
get_channel_pool,
AsyncResilientGRPCClient,
)
# Option 1: Use global pool directly
pool = get_channel_pool()
async with pool.pooled_channel("localhost:50051") as channel:
stub = MyServiceStub(channel)
response = await stub.Method(request)
# Option 2: Use resilient client with pooling
async with AsyncResilientGRPCClient(
host="localhost",
port=50051,
use_pool=True,
) as client:
result = await client.call_method(...)GRPCChannelPool (Async)
The async channel pool for grpc.aio.Channel.
Getting the Pool
from django_cfg.modules.django_grpc.services.client import (
get_channel_pool,
PoolConfig,
)
# Default configuration
pool = get_channel_pool()
# Custom configuration
config = PoolConfig(
max_size=50,
idle_timeout=300.0,
min_idle=5,
)
pool = get_channel_pool(config)Using pooled_channel Context Manager
The recommended way to use pooled channels:
async with pool.pooled_channel("localhost:50051") as channel:
stub = MyServiceStub(channel)
response = await stub.Method(request)
# Channel automatically returned to poolWith TLS:
async with pool.pooled_channel(
"secure-service:50051",
use_tls=True,
) as channel:
stub = SecureServiceStub(channel)
response = await stub.Method(request)Getting Channels Directly
For more control over channel lifecycle:
# Get channel (creates if needed)
channel = await pool.get_channel("localhost:50051")
# Use channel
stub = MyServiceStub(channel)
response = await stub.Method(request)
# Channel remains in pool for reusePool Statistics
stats = pool.get_stats()
print(f"Total channels: {stats['total_channels']}")
print(f"In use: {stats['channels_in_use']}")
print(f"Idle: {stats['channels_idle']}")
# Per-target stats
for target, target_stats in stats['pools'].items():
print(f" {target}: {target_stats['in_use']}/{target_stats['total']}")Closing the Pool
from django_cfg.modules.django_grpc.services.client import close_global_pool
# Close all channels and shutdown pool
await close_global_pool()SyncGRPCChannelPool
For synchronous code, use SyncGRPCChannelPool:
from django_cfg.modules.django_grpc.services.client import (
get_sync_channel_pool,
close_global_sync_pool,
)
# Get sync pool
pool = get_sync_channel_pool()
# Get channel
channel = pool.get_channel("localhost:50051")
# Use channel
stub = MyServiceStub(channel)
response = stub.Method(request)
# Close when done
close_global_sync_pool()Configuration
GrpcPoolConfig
The canonical pool configuration class. Import it from config.pool — it is the single source of truth, replacing older PoolConfig dataclass definitions.
from django_cfg.modules.django_grpc.config.pool import GrpcPoolConfig
config = GrpcPoolConfig(
enabled=True,
max_size=20,
idle_timeout=120.0,
min_idle=2,
max_age=3600.0,
cleanup_interval=60.0,
health_check_interval=30.0,
channel_ready_timeout=5.0,
)| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | True | Enable connection pooling |
max_size | int | 20 | Maximum channels in pool (1–200) |
idle_timeout | float | 120.0 | Seconds before an idle channel is closed (10–3600) |
min_idle | int | 2 | Minimum idle channels to maintain per address (0–20) |
max_age | float | 3600.0 | Maximum channel lifetime in seconds (60–86400) |
cleanup_interval | float | 60.0 | Seconds between pool cleanup cycles (10–600) |
health_check_interval | float | 30.0 | Seconds between idle channel health checks (5–600) |
channel_ready_timeout | float | 5.0 | Seconds to wait for a channel to become ready before giving up (1–60) |
Integration with DjangoGrpcModuleConfig
from django_cfg.modules.django_grpc.__cfg__ import DjangoGrpcModuleConfig
from django_cfg.modules.django_grpc.config.pool import GrpcPoolConfig
grpc_module = DjangoGrpcModuleConfig(
enabled=True,
pool=GrpcPoolConfig(
max_size=50,
idle_timeout=300.0,
health_check_interval=15.0,
),
)Integration with Clients
AsyncResilientGRPCClient
Enable pooling in the resilient client:
from django_cfg.modules.django_grpc.services.client import AsyncResilientGRPCClient
async with AsyncResilientGRPCClient(
host="localhost",
port=50051,
use_pool=True, # Enable pooling
) as client:
# Channels are reused across calls
result1 = await client.call_method(...)
result2 = await client.call_method(...)
# Check pool stats
print(client.pool_stats)Custom Pool Instance
Provide your own pool instance:
from django_cfg.modules.django_grpc.services.client import (
GRPCChannelPool,
PoolConfig,
AsyncResilientGRPCClient,
)
# Create custom pool
my_pool = GRPCChannelPool(PoolConfig(max_size=100))
# Use with client
async with AsyncResilientGRPCClient(
host="localhost",
port=50051,
use_pool=True,
pool=my_pool,
) as client:
result = await client.call_method(...)
# Clean up custom pool
await my_pool.close_all()Monitoring
Pool Health Check
def check_pool_health():
pool = get_channel_pool()
stats = pool.get_stats()
# Check utilization
utilization = stats['channels_in_use'] / max(stats['total_channels'], 1)
if utilization > 0.9:
print("WARNING: Pool near capacity")
if pool.is_closed:
print("ERROR: Pool is closed")
return {
"healthy": not pool.is_closed and utilization < 0.9,
"utilization": utilization,
"stats": stats,
}Metrics to Monitor
| Metric | Description | Alert Threshold |
|---|---|---|
total_channels | Channels in pool | Near max_size |
channels_in_use | Active channels | > 90% of total |
channels_idle | Available channels | < min_idle |
pool.is_closed | Pool status | True |
Performance Considerations
Pool Size Tuning
# Calculate based on concurrent requests
# Rule of thumb: max_size = peak_concurrent_requests * 1.2
config = PoolConfig(
max_size=int(expected_concurrent * 1.2),
min_idle=int(expected_concurrent * 0.2),
)Idle Timeout
# Short timeout for bursty traffic
config = PoolConfig(idle_timeout=60.0)
# Long timeout for steady traffic
config = PoolConfig(idle_timeout=300.0)Channel Age
# Rotate channels periodically for load balancer compatibility
config = PoolConfig(max_age=1800.0) # 30 minutesTroubleshooting
Pool Exhaustion
Symptom: Slow channel acquisition, timeouts.
Cause: All channels in use.
Solution:
# Increase pool size
config = PoolConfig(max_size=50)
# Check for leaks (unreleased channels)
stats = pool.get_stats()
print(f"In use: {stats['channels_in_use']}")Stale Channels
Symptom: Intermittent connection errors.
Cause: Channels kept too long.
Solution:
# Reduce max age
config = PoolConfig(max_age=1800.0)
# Reduce idle timeout
config = PoolConfig(idle_timeout=60.0)Memory Usage
Symptom: High memory consumption.
Cause: Too many idle channels.
Solution:
# Reduce pool size
config = PoolConfig(max_size=10)
# Reduce min idle
config = PoolConfig(min_idle=1)See Also
- Resilience Patterns - Retry and circuit breaker
- Configuration - Full configuration reference
- Troubleshooting - Common issues