Introduction
Knowing CMS commands is one thing; deploying CMS in production is another. This lesson covers battle-tested patterns for network traffic monitoring, click counting, query popularity tracking, and merging sketches across time windows.
Key Concepts
- Time-windowed sketches: Creating separate CMS instances per time period (minute, hour, day) and merging them for aggregate analysis.
- Sketch merging: Combining multiple CMS instances into one using CMS.MERGE, with optional weights for time-decay or importance.
- Layered counting: Using CMS alongside other structures (Bloom for dedup, Top-K for ranking) to build complete analytics pipelines.
Real World Context
Every major content platform needs to answer questions like "how many times was this video watched today?" or "which API endpoints are getting hammered?" CMS lets you answer these questions in kilobytes instead of gigabytes, even for millions of distinct items.
Deep Dive
Pattern 1: Network Traffic Monitoring
Track packet counts per source IP without storing every IP:
pythonimport redis from datetime import datetime r = redis.Redis() def track_packet(source_ip): """Record a packet from source_ip in current minute's sketch.""" minute = datetime.utcnow().strftime('%Y%m%d%H%M') key = f'traffic:{minute}' try: r.execute_command('CMS.INITBYPROB', key, 0.001, 0.01) r.expire(key, 7200) # Keep 2 hours of per-minute sketches except redis.ResponseError: pass r.execute_command('CMS.INCRBY', key, source_ip, 1) def get_ip_traffic(source_ip, minutes_back=60): """Estimate total packets from an IP over the last N minutes.""" now = datetime.utcnow() total = 0 for i in range(minutes_back): minute = (now - timedelta(minutes=i)).strftime('%Y%m%d%H%M') key = f'traffic:{minute}' try: result = r.execute_command('CMS.QUERY', key, source_ip) total += result[0] except redis.ResponseError: continue # Sketch doesn't exist for that minute return total
Pattern 2: Click Counting with Time Decay
Merge hourly sketches with decaying weights to emphasize recent clicks:
pythondef merge_with_decay(output_key, hourly_keys, decay=0.9): """Merge hourly sketches with exponential time decay. Most recent hour gets weight 1.0, previous gets 0.9, etc.""" existing_keys = [k for k in hourly_keys if r.exists(k)] if not existing_keys: return weights = [decay ** i for i in range(len(existing_keys))] weights.reverse() # Most recent last in list = highest weight args = [output_key, len(existing_keys)] + existing_keys + ['WEIGHTS'] + weights r.execute_command('CMS.MERGE', *args)
Pattern 3: Query Popularity Tracking
Track search query frequency to power autocomplete ranking:
pythonclass QueryPopularity: def __init__(self): self.daily_key = f'queries:{datetime.utcnow().strftime("%Y%m%d")}' try: r.execute_command('CMS.INITBYPROB', self.daily_key, 0.0001, 0.01) r.expire(self.daily_key, 86400 * 7) # Keep 7 days except redis.ResponseError: pass def record_query(self, query_text): """Record a search query.""" normalized = query_text.strip().lower() r.execute_command('CMS.INCRBY', self.daily_key, normalized, 1) def get_query_count(self, query_text): """Get estimated count for a query.""" normalized = query_text.strip().lower() result = r.execute_command('CMS.QUERY', self.daily_key, normalized) return result[0] def merge_weekly(self): """Merge last 7 daily sketches into a weekly aggregate.""" keys = [] for i in range(7): day = (datetime.utcnow() - timedelta(days=i)).strftime('%Y%m%d') key = f'queries:{day}' if r.exists(key): keys.append(key) if keys: week = datetime.utcnow().strftime('%Y-W%W') r.execute_command('CMS.MERGE', f'queries:week:{week}', len(keys), *keys)
Pattern 4: Merging Across Distributed Nodes
When running multiple application servers, each can maintain a local CMS. Periodically merge them:
redis# Each server writes to its own sketch CMS.INCRBY clicks:server1 "/home" 1 CMS.INCRBY clicks:server2 "/home" 1 # Periodically merge all server sketches CMS.MERGE clicks:combined 2 clicks:server1 clicks:server2 # Query the merged sketch CMS.QUERY clicks:combined "/home"
Common Pitfalls
- Merging sketches with different dimensions — CMS.MERGE requires all source sketches to have the same width and depth. Always use the same initialization parameters across sketches you plan to merge.
- Not setting TTL on time-windowed sketches — Without EXPIRE, old sketches accumulate forever. Always set a TTL when creating time-bucketed sketches.
Best Practices
- Use consistent naming conventions — Embed the time granularity in the key name (e.g.,
cms:clicks:20250312:14for hour 14 of March 12). This makes it easy to enumerate and merge. - Create sketches idempotently — Wrap CMS.INITBYPROB in a try/except since it errors if the key already exists. This makes your code safe for concurrent workers.
- Monitor with CMS.INFO — Periodically check the count field in CMS.INFO to ensure your sketches are receiving data and to estimate total stream size.
Summary
- Time-windowed sketches (per minute, hour, or day) keep absolute error bounded and enable flexible time-range queries via CMS.MERGE.
- CMS.MERGE with WEIGHTS enables time-decay patterns for recency-biased counting.
- Distributed systems can merge per-node sketches into a global view without shipping raw events.
- Always use consistent dimensions, set TTLs, and create sketches idempotently.
Code Examples
import redis
from datetime import datetime, timedelta
r = redis.Redis()
def create_hourly_sketch():
"""Create a CMS for the current hour."""
hour = datetime.utcnow().strftime('%Y%m%d%H')
key = f'analytics:{hour}'
try:
r.execute_command('CMS.INITBYPROB', key, 0.001, 0.01)
r.expire(key, 86400) # Keep for 24 hours
except redis.ResponseError:
pass
return key
def record_event(event_name):
"""Record an event in the current hour's sketch."""
key = create_hourly_sketch()
r.execute_command('CMS.INCRBY', key, event_name, 1)
def get_daily_count(event_name):
"""Merge last 24 hours and query an event's count."""
keys = []
now = datetime.utcnow()
for i in range(24):
hour = (now - timedelta(hours=i)).strftime('%Y%m%d%H')
key = f'analytics:{hour}'
if r.exists(key):
keys.append(key)
if not keys:
return 0
r.execute_command('CMS.MERGE', 'analytics:daily_tmp', len(keys), *keys)
result = r.execute_command('CMS.QUERY', 'analytics:daily_tmp', event_name)
r.delete('analytics:daily_tmp')
return result[0]