Building robust AI-powered applications requires more than just sending requests to an API endpoint. When I first deployed production workloads on HolySheep AI for a multilingual customer service chatbot serving 50,000 daily users, I encountered a sobering reality: approximately 3.2% of API calls fail due to network timeouts, rate limiting, server errors, or malformed responses. Without a proper retry architecture, this translates to 1,600 failed interactions per day — completely unacceptable for a business-critical system.

This hands-on guide walks through implementing production-grade error handling with dead letter queues (DLQ) and failure notifications using HolySheheep AI's API, tested across multiple scenarios with real latency measurements, success rate tracking, and integration complexity assessments.

Why Retry Strategies Matter in AI API Integrations

Modern AI APIs, including HolySheheep AI's unified endpoint at https://api.holysheep.ai/v1, operate under various constraints that can cause transient failures:

HolySheheep AI offers WeChat and Alipay payment options, supports models including GPT-4.1 at $8/MTok, Claude Sonnet 4.5 at $15/MTok, Gemini 2.5 Flash at $2.50/MTok, and DeepSeek V3.2 at just $0.42/MTok, with an exchange rate of ¥1=$1 that saves 85%+ compared to domestic alternatives priced at ¥7.3 per dollar equivalent. The platform consistently delivers <50ms latency for well-formed requests.

Architecture Overview: The Three-Layer Retry System

A production-ready retry architecture consists of three interconnected layers:

  1. Immediate Retry Layer — Handles transient errors with exponential backoff
  2. Dead Letter Queue (DLQ) — Captures permanently failed requests for manual review or replay
  3. Notification System — Alerts engineering teams when failure thresholds are exceeded

Implementation: HolySheheep AI Retry Client

The following Python implementation provides a complete, production-ready retry wrapper for HolySheheep AI's chat completions endpoint. This code handles all common error scenarios while maintaining audit trails for debugging.

import time
import json
import logging
from datetime import datetime, timedelta
from collections import deque
from dataclasses import dataclass, field
from typing import Optional, List, Dict, Any, Callable
from enum import Enum
import threading
import queue

Configure logging

logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) class RetryStrategy(Enum): EXPONENTIAL = "exponential" LINEAR = "linear" FIBONACCI = "fibonacci" @dataclass class APIError: """Structured error representation for HolySheheep AI API calls.""" error_type: str status_code: Optional[int] message: str timestamp: datetime = field(default_factory=datetime.now) request_id: Optional[str] = None retry_count: int = 0 resolved: bool = False resolution_method: Optional[str] = None @dataclass class DLQEntry: """Dead Letter Queue entry for failed API calls.""" original_request: Dict[str, Any] error: APIError payload_hash: str created_at: datetime = field(default_factory=datetime.now) retry_attempts: List[Dict[str, Any]] = field(default_factory=list) status: str = "pending" # pending, reprocessing, resolved, abandoned class HolySheheepRetryClient: """ Production-grade retry client for HolySheheep AI API. Features: - Configurable exponential backoff with jitter - Dead Letter Queue for permanent failures - Failure notifications via callback hooks - Thread-safe operation - Comprehensive logging and metrics """ BASE_URL = "https://api.holysheep.ai/v1" MAX_RETRIES = 5 BASE_DELAY = 1.0 MAX_DELAY = 60.0 TIMEOUT = 30.0 # HTTP status codes that are retryable RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504} # HTTP status codes that should NOT be retried NON_RETRYABLE_STATUS_CODES = {400, 401, 403, 404, 422} def __init__( self, api_key: str, base_url: str = BASE_URL, max_retries: int = MAX_RETRIES, base_delay: float = BASE_DELAY, max_delay: float = MAX_DELAY, enable_dlq: bool = True, dlq_max_size: int = 10000, notification_callback: Optional[Callable[[APIError], None]] = None, webhook_url: Optional[str] = None ): self.api_key = api_key self.base_url = base_url self.max_retries = max_retries self.base_delay = base_delay self.max_delay = max_delay self.enable_dlq = enable_dlq self.webhook_url = webhook_url # Thread-safe DLQ self._dlq_lock = threading.Lock() self._dlq: queue.Queue = queue.Queue(maxsize=dlq_max_size) # Thread-safe metrics self._metrics_lock = threading.Lock() self.metrics = { "total_requests": 0, "successful_requests": 0, "failed_requests": 0, "retried_requests": 0, "dlq_entries": 0, "average_latency_ms": 0.0, "latency_samples": deque(maxlen=1000) } # Notification callback self.notification_callback = notification_callback # Statistics tracking self._request_times: List[float] = [] logger.info(f"Initialized HolySheheepRetryClient with max_retries={max_retries}, " f"base_delay={base_delay}s, DLQ enabled={enable_dlq}") def _calculate_delay(self, attempt: int, strategy: RetryStrategy = RetryStrategy.EXPONENTIAL) -> float: """Calculate delay with exponential backoff and jitter.""" import random if strategy == RetryStrategy.EXPONENTIAL: delay = self.base_delay * (2 ** attempt) elif strategy == RetryStrategy.LINEAR: delay = self.base_delay * (attempt + 1) elif strategy == RetryStrategy.FIBONACCI: # Fibonacci backoff fib = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] delay = self.base_delay * fib[min(attempt, len(fib) - 1)] else: delay = self.base_delay * (2 ** attempt) # Cap at max_delay delay = min(delay, self.max_delay) # Add jitter (±25%) jitter = delay * 0.25 * (random.random() * 2 - 1) delay += jitter return delay def _is_retryable(self, status_code: Optional[int], error_message: str) -> bool: """Determine if an error is retryable.""" if status_code in self.NON_RETRYABLE_STATUS_CODES: return False if status_code in self.RETRYABLE_STATUS_CODES: return True # Check for specific error patterns non_retryable_patterns = [ "invalid_api_key", "authentication_failed", "permission_denied", "invalid_request_format", "model_not_found", "content_filter" ] for pattern in non_retryable_patterns: if pattern in error_message.lower(): return False return True def _send_notification(self, error: APIError): """Send failure notification via configured method.""" if self.notification_callback: try: self.notification_callback(error) except Exception as e: logger.error(f"Notification callback failed: {e}") if self.webhook_url: self._send_webhook_notification(error) def _send_webhook_notification(self, error: APIError): """Send notification to webhook URL (e.g., Slack, PagerDuty).""" import urllib.request import urllib.error payload = { "event": "api_failure", "error_type": error.error_type, "status_code": error.status_code, "message": error.message, "timestamp": error.timestamp.isoformat(), "retry_count": error.retry_count, "request_id": error.request_id } try: data = json.dumps(payload).encode('utf-8') req = urllib.request.Request( self.webhook_url, data=data, headers={'Content-Type': 'application/json'}, method='POST' ) with urllib.request.urlopen(req, timeout=5) as response: logger.info(f"Webhook notification sent: {response.status}") except Exception as e: logger.warning(f"Webhook notification failed: {e}") def _add_to_dlq(self, request: Dict[str, Any], error: APIError): """Add failed request to Dead Letter Queue.""" if not self.enable_dlq: return import hashlib # Create payload hash for deduplication payload_str = json.dumps(request, sort_keys=True) payload_hash = hashlib.sha256(payload_str.encode()).hexdigest()[:16] entry = DLQEntry( original_request=request, error=error, payload_hash=payload_hash ) try: self._dlq.put_nowait(entry) with self._metrics_lock: self.metrics["dlq_entries"] += 1 logger.warning(f"Added to DLQ: payload_hash={payload_hash}, " f"error={error.error_type}") except queue.Full: logger.error("DLQ is full! Consider increasing DLQ size or processing backlog.") def _make_request( self, endpoint: str, payload: Dict[str, Any], timeout: float = TIMEOUT ) -> tuple[Optional[Dict], Optional[APIError], float]: """ Make HTTP request to HolySheheep AI API. Returns: (response_data, error, latency_ms) """ import urllib.request import urllib.error import urllib.parse url = f"{self.base_url}/{endpoint.lstrip('/')}" start_time = time.time() try: data = json.dumps(payload).encode('utf-8') req = urllib.request.Request( url, data=data, headers={ 'Content-Type': 'application/json', 'Authorization': f'Bearer {self.api_key}', 'User-Agent': 'HolySheheep-RetryClient/1.0' }, method='POST' ) with urllib.request.urlopen(req, timeout=timeout) as response: latency_ms = (time.time() - start_time) * 1000 # Track latency with self._metrics_lock: self.metrics["latency_samples"].append(latency_ms) if self.metrics["latency_samples"]: self.metrics["average_latency_ms"] = sum( self.metrics["latency_samples"] ) / len(self.metrics["latency_samples"]) response_data = json.loads(response.read().decode('utf-8')) return response_data, None, latency_ms except urllib.error.HTTPError as e: latency_ms = (time.time() - start_time) * 1000 error_body = e.read().decode('utf-8') if e.fp else "" error = APIError( error_type="HTTPError", status_code=e.code, message=f"{e.reason}: {error_body[:500]}", request_id=e.headers.get('X-Request-ID') ) return None, error, latency_ms except urllib.error.URLError as e: latency_ms = (time.time() - start_time) * 1000 error = APIError( error_type="URLError", status_code=None, message=str(e.reason) ) return None, error, latency_ms except TimeoutError: latency_ms = (time.time() - start_time) * 1000 error = APIError( error_type="TimeoutError", status_code=None, message=f"Request timed out after {timeout}s" ) return None, error, latency_ms except Exception as e: latency_ms = (time.time() - start_time) * 1000 error = APIError( error_type="UnexpectedError", status_code=None, message=str(e) ) return None, error, latency_ms def chat_completions( self, messages: List[Dict[str, str]], model: str = "gpt-4.1", **kwargs ) -> tuple[Optional[Dict], Optional[APIError], Dict[str, Any]]: """ Send chat completion request with automatic retry logic. Returns: (response_data, error, metadata) Metadata includes: - retry_count: Number of retries performed - total_latency_ms: Total time including retries - was_cached: Whether response was served from cache - dlq_entry: Whether request was added to DLQ """ with self._metrics_lock: self.metrics["total_requests"] += 1 payload = { "model": model, "messages": messages, **kwargs } total_start_time = time.time() last_error: Optional[APIError] = None dlq_added = False for attempt in range(self.max_retries + 1): response, error, latency_ms = self._make_request( "chat/completions", payload ) if response is not None: with self._metrics_lock: self.metrics["successful_requests"] += 1 metadata = { "retry_count": attempt, "total_latency_ms": (time.time() - total_start_time) * 1000, "was_cached": response.get("cached", False), "dlq_entry": False } logger.info(f"Request succeeded after {attempt} retries, " f"latency={latency_ms:.2f}ms") return response, None, metadata # Request failed last_error = error last_error.retry_count = attempt logger.warning( f"Attempt {attempt + 1}/{self.max_retries + 1} failed: " f"error_type={error.error_type}, status={error.status_code}, " f"message={error.message[:100]}" ) # Check if retryable if not self._is_retryable(error.status_code, error.message): logger.error(f"Non-retryable error detected: {error.error_type}") break # Check if we have retries left if attempt >= self.max_retries: logger.error(f"Max retries ({self.max_retries}) exceeded") break # Calculate and apply delay delay = self._calculate_delay(attempt) # Special handling for rate limiting if error.status_code == 429: retry_after = self._parse_retry_after(error.message) if retry_after: delay = max(delay, retry_after) logger.info(f"Rate limited. Waiting {delay:.2f}s before retry") time.sleep(delay) with self._metrics_lock: self.metrics["retried_requests"] += 1 # All retries exhausted - add to DLQ if last_error: self._add_to_dlq(payload, last_error) self._send_notification(last_error) dlq_added = True with self._metrics_lock: self.metrics["failed_requests"] += 1 metadata = { "retry_count": self.max_retries, "total_latency_ms": (time.time() - total_start_time) * 1000, "was_cached": False, "dlq_entry": dlq_added } return None, last_error, metadata def _parse_retry_after(self, message: str) -> Optional[float]: """Parse Retry-After value from error message or headers.""" import re # Try to find retry-after in message match = re.search(r'retry[-_]after[:\s]*(\d+)', message, re.IGNORECASE) if match: return float(match.group(1)) return None def get_dlq_entries(self, count: int = 100) -> List[DLQEntry]: """Retrieve entries from Dead Letter Queue for reprocessing.""" entries = [] for _ in range(min(count, self._dlq.qsize())): try: entry = self._dlq.get_nowait() entries.append(entry) except queue.Empty: break return entries def reprocess_dlq_entry(self, entry: DLQEntry) -> bool: """Attempt to reprocess a DLQ entry.""" logger.info(f"Reprocessing DLQ entry: payload_hash={entry.payload_hash}") response, error, metadata = self.chat_completions( messages=entry.original_request.get("messages", []), model=entry.original_request.get("model", "gpt-4.1") ) if response: entry.status = "resolved" entry.error.resolved = True entry.error.resolution_method = "dlq_reprocess" logger.info(f"DLQ entry resolved: payload_hash={entry.payload_hash}") return True else: entry.status = "abandoned" logger.error(f"DLQ entry abandoned after reprocess: payload_hash={entry.payload_hash}") return False def get_metrics(self) -> Dict[str, Any]: """Get current client metrics.""" with self._metrics_lock: success_rate = 0.0 if self.metrics["total_requests"] > 0: success_rate = ( self.metrics["successful_requests"] / self.metrics["total_requests"] ) * 100 return { **self.metrics.copy(), "success_rate_percent": round(success_rate, 2), "dlq_size": self._dlq.qsize() }

Example notification callback

def slack_notification(error: APIError): """Send alert to Slack channel.""" import os # In production, use slack_sdk or similar print(f"ALERT: {error.error_type} - {error.message}")

Usage Example

if __name__ == "__main__": # Initialize client client = HolySheheepRetryClient( api_key="YOUR_HOLYSHEEP_API_KEY", # Replace with actual key max_retries=3, base_delay=1.0, enable_dlq=True, notification_callback=slack_notification ) # Make API call with automatic retry messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Explain API retry strategies in simple terms."} ] response, error, metadata = client.chat_completions( messages=messages, model="deepseek-v3.2", # Using cost-effective DeepSeek V3.2 at $0.42/MTok temperature=0.7, max_tokens=500 ) if response: print(f"Success! Response: {response['choices'][0]['message']['content'][:200]}...") print(f"Metadata: {metadata}") else: print(f"Request failed: {error.message}") print(f"Added to DLQ: {metadata['dlq_entry']}") # Print metrics print(f"\nClient Metrics: {client.get_metrics()}")

Dead Letter Queue Management Best Practices

The DLQ implementation above captures failed requests with full context for later analysis. However, capturing data is only half the battle — you need a strategy for processing these entries.

# dlq_processor.py - Production DLQ Processing System

import json
import logging
from datetime import datetime, timedelta
from typing import List, Dict, Any
from collections import Counter
import hashlib

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class DLQProcessor:
    """
    Production-grade DLQ processor with:
    - Automatic categorization and triage
    - Batch reprocessing with backpressure
    - Alert escalation for critical failures
    - Metrics and reporting
    """
    
    def __init__(self, client: 'HolySheheepRetryClient', alert_threshold: int = 100):
        self.client = client
        self.alert_threshold = alert_threshold
        self.categorized_errors: Dict[str, List[Dict]] = {}
        
    def categorize_dlq_entries(self) -> Dict[str, List[Dict]]:
        """Categorize DLQ entries by error type for targeted resolution."""
        entries = self.client.get_dlq_entries(count=1000)
        categorized = {
            "rate_limited": [],
            "server_errors": [],
            "timeout_errors": [],
            "auth_errors": [],
            "validation_errors": [],
            "unknown_errors": []
        }
        
        for entry in entries:
            error = entry.error
            entry_dict = {
                "payload_hash": entry.payload_hash,
                "error_message": error.message,
                "status_code": error.status_code,
                "created_at": entry.created_at.isoformat(),
                "retry_count": error.retry_count,
                "model": entry.original_request.get("model", "unknown")
            }
            
            if error.status_code == 429:
                categorized["rate_limited"].append(entry_dict)
            elif error.status_code and 500 <= error.status_code < 600:
                categorized["server_errors"].append(entry_dict)
            elif error.error_type == "TimeoutError":
                categorized["timeout_errors"].append(entry_dict)
            elif error.status_code in {401, 403}:
                categorized["auth_errors"].append(entry_dict)
            elif error.status_code in {400, 422}:
                categorized["validation_errors"].append(entry_dict)
            else:
                categorized["unknown_errors"].append(entry_dict)
        
        self.categorized_errors = categorized
        return categorized
    
    def generate_dlq_report(self) -> str:
        """Generate human-readable DLQ status report."""
        categorized = self.categorize_dlq_entries()
        
        report_lines = [
            "=" * 60,
            "DEAD LETTER QUEUE STATUS REPORT",
            f"Generated: {datetime.now().isoformat()}",
            "=" * 60,
            ""
        ]
        
        total_entries = 0
        for category, entries in categorized.items():
            count = len(entries)
            total_entries += count
            report_lines.append(f"{category.upper().replace('_', ' ')}: {count}")
            
            if entries and count <= 5:
                for entry in entries:
                    report_lines.append(f"  - {entry['error_message'][:80]}...")
            elif entries:
                report_lines.append(f"  (showing 5 of {count} entries)")
                for entry in entries[:5]:
                    report_lines.append(f"  - {entry['error_message'][:80]}...")
        
        report_lines.extend([
            "",
            f"TOTAL DLQ ENTRIES: {total_entries}",
            ""
        ])
        
        # Error distribution
        if categorized["validation_errors"]:
            report_lines.append("VALIDATION ERROR BREAKDOWN:")
            error_messages = [e['error_message'] for e in categorized["validation_errors"]]
            error_counts = Counter(error_messages)
            for msg, count in error_counts.most_common(5):
                report_lines.append(f"  [{count}x] {msg[:60]}")
        
        # Alert recommendations
        if total_entries > self.alert_threshold:
            report_lines.extend([
                "",
                "⚠️  ALERT: DLQ size exceeds threshold!",
                "Recommended actions:",
                "  1. Check HolySheheep AI status page",
                "  2. Review rate limiting configuration",
                "  3. Consider scaling retry capacity"
            ])
        
        return "\n".join(report_lines)
    
    def batch_reprocess(
        self,
        category: str = "all",
        batch_size: int = 50,
        delay_between_batches: float = 5.0
    ) -> Dict[str, Any]:
        """
        Attempt to reprocess DLQ entries in batches.
        
        Returns:
            Statistics on reprocessing results
        """
        import time
        
        categorized = self.categorize_dlq_entries()
        
        if category == "all":
            entries_to_process = self.client.get_dlq_entries(count=batch_size)
        else:
            # Get entries by category (simplified - in production, use proper filtering)
            entries_to_process = self.client.get_dlq_entries(count=batch_size)
        
        results = {
            "attempted": 0,
            "succeeded": 0,
            "failed": 0,
            "skipped": 0,
            "category": category,
            "timestamp": datetime.now().isoformat()
        }
        
        for entry in entries_to_process:
            results["attempted"] += 1
            
            # Skip non-retryable errors
            if entry.error.status_code in {400, 401, 403, 422}:
                results["skipped"] += 1
                entry.status = "abandoned"
                continue
            
            # Attempt reprocessing
            success = self.client.reprocess_dlq_entry(entry)
            
            if success:
                results["succeeded"] += 1
            else:
                results["failed"] += 1
            
            # Rate limiting protection
            time.sleep(0.5)
        
        logger.info(f"Batch reprocessing complete: {results}")
        return results
    
    def export_dlq_for_analysis(self, filepath: str = "dlq_export.json"):
        """Export DLQ entries to JSON for external analysis."""
        categorized = self.categorize_dlq_entries()
        
        export_data = {
            "export_timestamp": datetime.now().isoformat(),
            "total_entries": sum(len(v) for v in categorized.values()),
            "categories": categorized
        }
        
        with open(filepath, 'w', encoding='utf-8') as f:
            json.dump(export_data, f, indent=2, ensure_ascii=False)
        
        logger.info(f"DLQ exported to {filepath}")
        return filepath


Webhook notification handler for production alerting

class DLQWebhookHandler: """Handle incoming webhook notifications for DLQ events.""" def __init__(self, processor: DLQProcessor): self.processor = processor def handle_slack_webhook(self, payload: Dict[str, Any]) -> Dict[str, Any]: """Process incoming Slack webhook and generate DLQ summary.""" logger.info(f"Received Slack webhook: {payload}") # Generate quick summary categorized = self.processor.categorize_dlq_entries() return { "response_action": "respond", "text": f"🔴 DLQ Alert: {sum(len(v) for v in categorized.values())} failed requests", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": f"*🔴 HolySheheep AI DLQ Alert*\n" f"Total Failed Requests: {sum(len(v) for v in categorized.values())}" } }, { "type": "section", "fields": [ {"type": "mrkdwn", "text": f"*Rate Limited:*\n{len(categorized['rate_limited'])}"}, {"type": "mrkdwn", "text": f"*Server Errors:*\n{len(categorized['server_errors'])}"}, {"type": "mrkdwn", "text": f"*Timeouts:*\n{len(categorized['timeout_errors'])}"}, {"type": "mrkdwn", "text": f"*Auth Errors:*\n{len(categorized['auth_errors'])}"} ] } ] } def handle_pagerduty_webhook(self, payload: Dict[str, Any]) -> Dict[str, Any]: """Generate PagerDuty incident for critical DLQ overflow.""" categorized = self.processor.categorize_dlq_entries() total = sum(len(v) for v in categorized.values()) return { "routing_key": "YOUR_PAGERDUTY_KEY", "event_action": "trigger", "payload": { "summary": f"HolySheheep AI DLQ overflow: {total} failed requests", "severity": "error" if total > 100 else "warning", "source": "holysheep-dlq-processor", "custom_details": { "dlq_size": total, "error_breakdown": {k: len(v) for k, v in categorized.items()}, "urgent_action_required": total > 500 } } } if __name__ == "__main__": from dlq_client import HolySheheepRetryClient # Initialize with your API key client = HolySheheepRetryClient( api_key="YOUR_HOLYSHEEP_API_KEY", max_retries=3 ) # Initialize processor processor = DLQProcessor(client, alert_threshold=100) # Generate report print(processor.generate_dlq_report()) # Export for analysis processor.export_dlq_for_analysis() # Attempt batch reprocessing results = processor.batch_reprocess( category="server_errors", batch_size=25 ) print(f"\nReprocessing Results: {results}")

Failure Notification Systems

Effective alerting prevents cascading failures and ensures rapid response to systemic issues. Here's a comprehensive notification system that integrates with multiple channels:

# notification_system.py - Multi-channel failure notifications

import logging
import json
import threading
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Callable
from dataclasses import dataclass, field
from enum import Enum
from collections import deque
import time
import hashlib

import urllib.request
import urllib.error

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class AlertSeverity(Enum):
    INFO = "info"
    WARNING = "warning"
    ERROR = "error"
    CRITICAL = "critical"


@dataclass
class Alert:
    """Structured alert representation."""
    id: str
    title: str
    message: str
    severity: AlertSeverity
    timestamp: datetime
    source: str
    metadata: Dict[str, Any] = field(default_factory=dict)
    acknowledged: bool = False
    resolved: bool = False


class AlertManager:
    """
    Centralized alert management with:
    - Rate limiting to prevent alert storms
    - Aggregation of similar alerts
    - Multi-channel delivery
    - Alert correlation
    """
    
    def __init__(
        self,
        rate_limit_seconds: int = 60,
        aggregation_window_seconds: int = 300,
        max_alerts_per_window: int = 50
    ):
        self.rate_limit_seconds = rate_limit_seconds
        self.aggregation_window = timedelta(seconds=aggregation_window_seconds)
        self.max_alerts_per_window = max_alerts_per_window
        
        self._alerts_lock = threading.Lock()
        self._alerts: deque = deque(maxlen=10000)
        self._alert_counts: Dict[str, int] = {}
        self._last_alert_times: Dict[str, float] = {}
        
        self._channels: List['NotificationChannel'] = []
        self._aggregation_buffer: Dict[str, List[Alert]] = {}
        self._aggregation_timer: Optional[threading.Timer] = None
        
        logger.info(f"AlertManager initialized with rate_limit={rate_limit_seconds}s, "
                   f"aggregation_window={aggregation_window_seconds}s")
    
    def add_channel(self, channel: 'NotificationChannel'):
        """Add a notification channel."""
        self._channels.append(channel)
        logger.info(f"Added notification channel: {channel.name}")
    
    def send_alert(self, alert: Alert) -> bool:
        """
        Send alert through all configured channels.
        Implements rate limiting and aggregation.
        """
        alert.id = hashlib.md5(
            f"{alert.title}{alert.timestamp}".encode()
        ).hexdigest()[:16]
        
        # Check rate limit
        if not self._check_rate_limit(alert):
            logger.debug(f"Alert rate limited: {alert.title}")
            return False
        
        # Track alert
        with self._alerts_lock:
            self._alerts.append(alert)
            self._alert_counts[alert.severity.value] = \
                self._alert_counts.get(alert.severity.value, 0) + 1
        
        # Aggregate similar alerts
        aggregation_key = f"{alert.severity.value}:{alert.source}"
        if aggregation_key not in self._aggregation_buffer:
            self._aggregation_buffer[aggregation_key] = []
        self._aggregation_buffer[aggregation_key].append(alert)
        
        # Send to all channels
        success = True
        for channel in self._channels:
            try:
                channel.send(alert)
            except Exception as e:
                logger.error(f"Channel {channel.name} failed: {e}")
                success = False
        
        return success
    
    def _check_rate_limit(self, alert: Alert) -> bool:
        """Check if alert should be sent based on rate limiting."""
        alert_key = f"{alert.severity.value}:{alert.source}"
        current_time = time.time()
        
        # Check total rate limit
        total_today = sum(self._alert_counts.values())
        if total_today >= self.max_alerts_per_window:
            return False
        
        # Check per-source rate limit
        if alert_key in self._last_alert_times:
            time_since_last = current_time - self._last_alert_times[alert_key]
            if time_since_last < self.rate_limit_seconds:
                return False
        
        self._last_alert_times[alert_key] = current_time
        return True
    
    def flush_aggregated_alerts(self):
        """Send aggregated alerts to channels."""
        for aggregation_key, alerts in self._aggregation_buffer.items():
            if not alerts:
                continue
            
            # Create summary alert
            severity = alerts[0].severity
            source = alerts[0].source
            count = len(alerts)
            
            summary_alert = Alert(
                id=hashlib.md5(aggregation_key.encode()).hexdigest()[:16],
                title=f"[BATCH] {alerts[0].title}",
                message=f"{count} similar alerts in aggregation window: " +