Issue: #743 - Memory Optimization Status: Implemented (already present, now documented and optimized)
AutoBot implements singleton connection pooling for all Redis connections. This document explains how the pooling works and why it's memory-efficient.
The RedisConnectionManager class maintains singleton connection pools:
# In RedisConnectionManager
self._sync_pools: Dict[str, ConnectionPool] = {} # One sync pool per database
self._async_pools: Dict[str, ConnectionPool] = {} # One async pool per database- First Request: Pool is created and stored in dictionary
- Subsequent Requests: Same pool is reused from dictionary
- Connection Reuse: Connections are borrowed from pool and returned after use
from src.utils.redis_client import get_redis_client
# First call creates pool with max 20 connections
client1 = get_redis_client(database="main") # Pool created, stored in _sync_pools["main"]
# Next 99 calls reuse THE SAME pool
for i in range(99):
client = get_redis_client(database="main") # Pool reused from _sync_pools["main"]
# Memory usage: 1 pool with max 20 connections
# NOT: 100 pools with 2000 connections# src/constants/redis_constants.py
class RedisConnectionConfig:
MAX_CONNECTIONS_POOL: int = 20 # Optimized from 100 for memory efficiency
SOCKET_TIMEOUT: int = 5
SOCKET_CONNECT_TIMEOUT: int = 3- Typical concurrency: Most operations are sequential, not parallel
- Memory footprint: 20 connections per pool keeps memory usage low
- Performance: Sufficient for high-throughput workloads
- Per-database isolation: Each database has its own pool of 20
With 13 databases, maximum connections = 13 databases × 20 connections = 260 connections
In practice, pools are created lazily (only when needed), so actual usage is much lower.
def get_sync_client(self, database_name: str = "main"):
# Check if pool exists
if database_name not in self._sync_pools:
with self._init_lock: # Thread-safe creation
if database_name not in self._sync_pools:
# Create pool ONCE
self._sync_pools[database_name] = self._create_sync_pool(database_name)
# Reuse pool for all requests
client = redis.Redis(connection_pool=self._sync_pools[database_name])
return client1. get_redis_client() called
2. Check pool cache (_sync_pools)
3. If missing: Create pool (max 20 connections)
4. If exists: Reuse pool
5. Borrow connection from pool
6. Execute Redis operation
7. Return connection to pool (automatic)
Prevents cascading failures when Redis is unavailable:
- Tracks failures per database
- Opens circuit after 5 failures (configurable)
- Blocks requests for 60 seconds
- Auto-resets after timeout
Prevents connection drops on idle connections:
socket.TCP_KEEPIDLE: 600 # 10 minutes before first probe
socket.TCP_KEEPINTVL: 60 # 1 minute between probes
socket.TCP_KEEPCNT: 5 # 5 failed probes before disconnectAutomatically removes idle connections after 5 minutes:
- Background task runs every 60 seconds
- Checks for connections idle > 300 seconds
- Closes idle connections to free resources
Exponential backoff for failed connections:
- Base wait: 2 seconds
- Max wait: 30 seconds
- Max attempts: 5
- Only retries on connection errors
from src.utils.redis_client import get_redis_client
# Get client (creates pool on first call)
redis = get_redis_client(database="main")
# Use client
redis.set("key", "value")
result = redis.get("key")
# Connection automatically returned to poolfrom src.utils.redis_client import get_redis_client
async def store_data():
# Get async client (creates async pool on first call)
redis = await get_redis_client(async_client=True, database="main")
# Use client
await redis.set("key", "value")
result = await redis.get("key")
# Connection automatically returned to poolfrom src.utils.redis_client import get_redis_client
async def batch_operations():
manager = RedisConnectionManager()
# Pipeline uses pooled connection
async with manager.pipeline("main") as pipe:
pipe.set("key1", "value1")
pipe.set("key2", "value2")
pipe.set("key3", "value3")
# Auto-executes on context exitmanager = RedisConnectionManager()
# Each database has its own pool
main_client = await manager.main() # Pool for db 0
knowledge_client = await manager.knowledge() # Pool for db 1
prompts_client = await manager.prompts() # Pool for db 2from src.utils.redis_client import get_redis_client
manager = RedisConnectionManager()
# Get pool statistics for specific database
stats = manager.get_pool_statistics("main")
print(f"Created connections: {stats.created_connections}")
print(f"Available connections: {stats.available_connections}")
print(f"In-use connections: {stats.in_use_connections}")
print(f"Max connections: {stats.max_connections}")
print(f"Idle connections: {stats.idle_connections}")health = manager.get_health_status()
print(f"Overall healthy: {health['overall_healthy']}")
print(f"Total databases: {health['total_databases']}")
print(f"Healthy databases: {health['healthy_databases']}")
print(f"Success rate: {health['success_rate']}%")MAX_CONNECTIONS_POOL: 100- Potential: 13 databases × 100 connections = 1,300 connections
- High memory footprint under load
MAX_CONNECTIONS_POOL: 20- Maximum: 13 databases × 20 connections = 260 connections
- 80% reduction in maximum connection count
- Actual usage much lower due to lazy pool creation
Each Redis connection uses approximately:
- 4 KB for socket buffers
- 16 KB for Python object overhead
- ~20 KB total per connection
Savings: (1,300 - 260) × 20 KB = 20.8 MB maximum memory reduction
All existing code continues to work unchanged:
# OLD code (still works)
redis = get_redis_client(database="main")
# NEW code (same behavior)
redis = get_redis_client(database="main")The pooling is transparent to callers. The only change is improved memory efficiency.
To customize pool size for specific use cases:
# config/redis-databases.yaml
redis_databases:
main:
db: 0
max_connections: 50 # Override default 20Or via environment variables:
export AUTOBOT_REDIS_MAX_CONNECTIONS=50from src.utils.redis_client import get_redis_client
from src.utils.redis_management.connection_manager import RedisConnectionManager
manager = RedisConnectionManager()
# Get multiple clients
client1 = get_redis_client(database="main")
client2 = get_redis_client(database="main")
client3 = get_redis_client(database="main")
# Verify they share the same pool
pool1 = client1.connection_pool
pool2 = client2.connection_pool
pool3 = client3.connection_pool
assert pool1 is pool2 is pool3, "Clients should share the same pool"
print("✅ Pooling verified: All clients share the same connection pool")
# Check pool statistics
stats = manager.get_pool_statistics("main")
print(f"Pool has {stats.created_connections} connections created")
print(f"Max connections: {stats.max_connections}")import asyncio
from src.utils.redis_client import get_redis_client
async def test_concurrent_operations():
"""Test that pool handles concurrent operations efficiently."""
tasks = []
for i in range(100): # 100 concurrent operations
async def operation():
redis = await get_redis_client(async_client=True, database="main")
await redis.set(f"test_key_{i}", f"value_{i}")
return await redis.get(f"test_key_{i}")
tasks.append(operation())
results = await asyncio.gather(*tasks)
print(f"✅ Completed {len(results)} concurrent operations")
print("✅ Pool efficiently handled concurrency with max 20 connections")
# Run test
asyncio.run(test_concurrent_operations())If you see connection limit errors:
-
Check current pool usage:
stats = manager.get_pool_statistics("main") print(f"In-use connections: {stats.in_use_connections}")
-
Increase max_connections if needed:
# config/redis-databases.yaml max_connections: 50
-
Check for connection leaks (connections not returned to pool)
If circuit breaker is blocking requests:
- Check Redis server health
- Review error logs
- Wait for timeout (60 seconds) or restart application
If statistics seem stale:
- Ensure you're checking the correct database name
- Verify pool has been created (not lazy-loaded yet)
- Check manager initialization
- Implementation:
src/utils/redis_management/connection_manager.py - Configuration:
src/constants/redis_constants.py - Main Interface:
src/utils/redis_client.py - Issue: #743 - Memory Optimization