Throttle Outbound Cognigy Webhook Requests with Python Rate Limiting and Queue Management
What You Will Build
- A Python module that queues, validates, and throttles outbound HTTP requests to Cognigy webhook endpoints using configurable rate matrices, automatic queue shedding, and health verification pipelines.
- This tutorial uses the Cognigy.AI REST API v2 (
/api/v2/webhooks) and Python 3.10 withhttpxandpydantic. - The code covers authentication, schema validation, async queue management, latency tracking, audit logging, and callback synchronization.
Prerequisites
- Cognigy.AI OAuth2 client credentials with scopes
webhooks:readandwebhooks:write - Cognigy API version
v2 - Python 3.10 or higher
- External dependencies:
httpx==0.27.0,pydantic==2.6.0,asyncio(standard library) - Install dependencies:
pip install httpx pydantic
Authentication Setup
Cognigy uses standard OAuth2 client credentials flow. The following code retrieves an access token and caches it with automatic refresh logic.
import httpx
import asyncio
import time
from typing import Optional
from dataclasses import dataclass, field
@dataclass
class CognigyAuthConfig:
tenant: str
client_id: str
client_secret: str
base_url: str = "https://{tenant}.cognigy.ai"
token_url: str = "https://{tenant}.cognigy.ai/oauth/token"
scopes: list[str] = field(default_factory=lambda: ["webhooks:read", "webhooks:write"])
class CognigyTokenManager:
def __init__(self, config: CognigyAuthConfig):
self.config = config
self.base_url = config.base_url.format(tenant=config.tenant)
self.token_url = config.token_url.format(tenant=config.tenant)
self.access_token: Optional[str] = None
self.token_expiry: float = 0.0
self.client = httpx.AsyncClient(timeout=10.0)
async def get_token(self) -> str:
if self.access_token and time.time() < self.token_expiry - 30:
return self.access_token
payload = {
"grant_type": "client_credentials",
"client_id": self.config.client_id,
"client_secret": self.config.client_secret,
"scope": " ".join(self.config.scopes)
}
response = await self.client.post(self.token_url, data=payload)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data["access_token"]
self.token_expiry = time.time() + token_data["expires_in"]
return self.access_token
async def close(self):
await self.client.aclose()
Implementation
Step 1: Schema Validation and Payload Construction
Cognigy webhook payloads must conform to specific structural constraints. The following Pydantic models enforce format verification against gateway limits.
from pydantic import BaseModel, Field, field_validator
from typing import Any, Optional
import uuid
class CognigyWebhookPayload(BaseModel):
webhook_id: str = Field(..., description="Unique identifier for the webhook endpoint")
event_type: str = Field(..., pattern=r"^[a-zA-Z0-9_-]+$")
payload_version: int = Field(default=2, ge=1, le=3)
data: dict[str, Any] = Field(default_factory=dict)
metadata: dict[str, Any] = Field(default_factory=dict)
@field_validator("webhook_id")
@classmethod
def validate_webhook_id_format(cls, v: str) -> str:
if len(v) > 128:
raise ValueError("webhook_id exceeds maximum gateway constraint of 128 characters")
return v
@field_validator("data")
@classmethod
def validate_payload_size(cls, v: dict) -> dict:
import json
size_bytes = len(json.dumps(v).encode("utf-8"))
if size_bytes > 256_000:
raise ValueError("Payload exceeds 256KB maximum queue depth limit")
return v
class ThrottleDirective(BaseModel):
webhook_id: str
rate_limit_matrix: dict[str, int] = Field(
default_factory=lambda: {"requests_per_second": 10, "burst_allowance": 25}
)
queue_overflow_action: str = Field(default="shed", pattern=r"^(shed|queue|reject)$")
max_queue_depth: int = Field(default=100, ge=1, le=1000)
health_check_interval_sec: float = Field(default=30.0, gt=0)
Step 2: Rate Limit Matrix and Queue Management
This step implements atomic internal operations for request pacing. The queue enforces maximum depth limits and triggers automatic shedding when overflow occurs.
import asyncio
import logging
from collections import deque
from datetime import datetime, timezone
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("cognigy_throttler")
class WebhookRequestQueue:
def __init__(self, directive: ThrottleDirective):
self.directive = directive
self.queue: deque = deque(maxlen=directive.max_queue_depth)
self.active_semaphore = asyncio.Semaphore(directive.rate_limit_matrix["burst_allowance"])
self.last_request_time: float = 0.0
self.request_count: int = 0
self.window_start: float = time.time()
async def enqueue(self, payload: CognigyWebhookPayload) -> bool:
if len(self.queue) >= self.directive.max_queue_depth:
logger.warning(
f"Queue overflow detected for {self.directive.webhook_id}. "
f"Action: {self.directive.queue_overflow_action}"
)
if self.directive.queue_overflow_action == "shed":
dropped = self.queue.popleft()
logger.info(f"Shed oldest request to maintain queue stability")
elif self.directive.queue_overflow_action == "reject":
return False
elif self.directive.queue_overflow_action == "queue":
logger.info("Queue at capacity. Holding new request until space frees.")
await asyncio.sleep(0.1)
return await self.enqueue(payload)
self.queue.append({"payload": payload, "enqueued_at": time.time()})
return True
async def acquire_rate_token(self) -> None:
await self.active_semaphore.acquire()
now = time.time()
if now - self.window_start >= 1.0:
self.request_count = 0
self.window_start = now
if self.request_count >= self.directive.rate_limit_matrix["requests_per_second"]:
sleep_time = 1.0 - (now - self.window_start)
if sleep_time > 0:
await asyncio.sleep(sleep_time)
self.request_count += 1
self.last_request_time = time.time()
def release_rate_token(self) -> None:
self.active_semaphore.release()
Step 3: Health Checking, Burst Verification, and Throttle Execution
This pipeline validates endpoint health, verifies burst allowance, executes the outbound request, and synchronizes throttling events with external monitors via callbacks.
from typing import Callable, Awaitable
HealthCallback = Callable[[dict], Awaitable[None]]
class CognigyWebhookThrottler:
def __init__(
self,
auth: CognigyTokenManager,
directive: ThrottleDirective,
on_throttle_event: Optional[HealthCallback] = None
):
self.auth = auth
self.directive = directive
self.queue_manager = WebhookRequestQueue(directive)
self.callback = on_throttle_event
self.audit_log: list[dict] = []
self.success_count: int = 0
self.failure_count: int = 0
self.total_latency_ms: float = 0.0
self.client = httpx.AsyncClient(timeout=15.0, limits=httpx.Limits(max_connections=50))
async def check_endpoint_health(self, webhook_id: str) -> bool:
try:
token = await self.auth.get_token()
url = f"{self.auth.base_url}/api/v2/webhooks/{webhook_id}"
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
response = await self.client.get(url, headers=headers)
return response.status_code == 200
except httpx.HTTPStatusError as e:
if e.response.status_code in (403, 401):
logger.error(f"Authentication failure for {webhook_id}: {e}")
return False
logger.warning(f"Health check failed for {webhook_id}: {e}")
return False
except Exception as e:
logger.error(f"Health check exception for {webhook_id}: {e}")
return False
async def dispatch_webhook(self, payload: CognigyWebhookPayload) -> dict:
token = await self.auth.get_token()
url = f"{self.auth.base_url}/api/v2/webhooks"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"X-Cognigy-Webhook-Id": payload.webhook_id
}
start_time = time.time()
try:
response = await self.client.post(url, headers=headers, json=payload.model_dump())
response.raise_for_status()
latency_ms = (time.time() - start_time) * 1000
self.success_count += 1
self.total_latency_ms += latency_ms
result = {
"status": "success",
"http_status": response.status_code,
"latency_ms": latency_ms,
"webhook_id": payload.webhook_id
}
await self._record_audit(result)
if self.callback:
await self.callback(result)
return result
except httpx.HTTPStatusError as e:
latency_ms = (time.time() - start_time) * 1000
self.failure_count += 1
self.total_latency_ms += latency_ms
if e.response.status_code == 429:
retry_after = float(e.response.headers.get("Retry-After", 1.0))
logger.warning(f"Rate limited by provider. Waiting {retry_after}s")
await asyncio.sleep(retry_after)
result = {
"status": "failure",
"http_status": e.response.status_code,
"latency_ms": latency_ms,
"webhook_id": payload.webhook_id,
"error": str(e)
}
await self._record_audit(result)
if self.callback:
await self.callback(result)
return result
except Exception as e:
result = {
"status": "error",
"webhook_id": payload.webhook_id,
"error": str(e)
}
await self._record_audit(result)
raise
async def _record_audit(self, event: dict) -> None:
audit_entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"event_type": "throttle_dispatch",
"webhook_id": event.get("webhook_id"),
"outcome": event.get("status"),
"latency_ms": event.get("latency_ms", 0),
"success_rate": self._calculate_success_rate(),
"avg_latency_ms": self._calculate_avg_latency()
}
self.audit_log.append(audit_entry)
logger.info(f"Audit: {audit_entry}")
def _calculate_success_rate(self) -> float:
total = self.success_count + self.failure_count
return (self.success_count / total * 100) if total > 0 else 0.0
def _calculate_avg_latency(self) -> float:
total = self.success_count + self.failure_count
return (self.total_latency_ms / total) if total > 0 else 0.0
async def run_throttle_loop(self) -> None:
logger.info(f"Starting throttle loop for {self.directive.webhook_id}")
is_healthy = await self.check_endpoint_health(self.directive.webhook_id)
if not is_healthy:
logger.error("Endpoint health check failed. Pausing dispatch loop.")
return
while True:
if not self.queue_manager.queue:
await asyncio.sleep(0.5)
continue
await self.queue_manager.acquire_rate_token()
item = self.queue_manager.queue.popleft()
payload = item["payload"]
try:
await self.dispatch_webhook(payload)
except Exception as e:
logger.error(f"Dispatch failed: {e}")
finally:
self.queue_manager.release_rate_token()
async def close(self):
await self.client.aclose()
await self.auth.close()
Complete Working Example
The following script initializes the throttler, populates the queue, and runs the dispatch loop. Replace credentials with your Cognigy tenant values.
import asyncio
import logging
async def main():
auth_config = CognigyAuthConfig(
tenant="your-tenant-name",
client_id="your_client_id",
client_secret="your_client_secret"
)
directive = ThrottleDirective(
webhook_id="wh_prod_cognigy_integration_01",
rate_limit_matrix={"requests_per_second": 5, "burst_allowance": 10},
queue_overflow_action="shed",
max_queue_depth=50
)
async def monitor_callback(event: dict) -> None:
logging.info(f"External Monitor Sync: {event['status']} | Latency: {event.get('latency_ms', 0):.2f}ms")
auth_manager = CognigyTokenManager(auth_config)
throttler = CognigyWebhookThrottler(
auth=auth_manager,
directive=directive,
on_throttle_event=monitor_callback
)
try:
sample_payload = CognigyWebhookPayload(
webhook_id="wh_prod_cognigy_integration_01",
event_type="session_update",
payload_version=2,
data={"session_id": "sess_8821", "user_state": "active", "context": {"lang": "en"}},
metadata={"source": "automation_engine", "priority": "high"}
)
await throttler.queue_manager.enqueue(sample_payload)
logging.info("Payload queued. Starting throttle execution.")
await throttler.run_throttle_loop()
except KeyboardInterrupt:
logging.info("Throttle loop interrupted by user.")
finally:
await throttler.close()
if __name__ == "__main__":
asyncio.run(main())
Common Errors & Debugging
Error: 401 Unauthorized
- What causes it: Expired OAuth token, incorrect client credentials, or missing
webhooks:writescope. - How to fix it: Verify the
client_secretmatches the Cognigy admin console configuration. Ensure the token refresh logic subtracts a 30-second buffer before expiry. - Code showing the fix: The
CognigyTokenManager.get_tokenmethod automatically refreshes whentime.time() >= self.token_expiry - 30.
Error: 429 Too Many Requests
- What causes it: The Cognigy gateway enforces platform-level rate limits that exceed your local matrix configuration.
- How to fix it: Parse the
Retry-Afterheader and pause the dispatch loop. Thedispatch_webhookmethod already implements this fallback. - Code showing the fix:
if e.response.status_code == 429:
retry_after = float(e.response.headers.get("Retry-After", 1.0))
await asyncio.sleep(retry_after)
Error: Queue Overflow Exception
- What causes it: Inbound payload generation exceeds the configured
max_queue_depthfaster than the dispatcher can process them. - How to fix it: Adjust the
queue_overflow_actiontoshedorrejectbased on business tolerance. Increasemax_queue_depthif memory permits. - Code showing the fix: The
WebhookRequestQueue.enqueuemethod evaluates the directive action and either drops the oldest item or rejects the new one to prevent memory exhaustion.
Error: Pydantic ValidationError
- What causes it: Payload exceeds 256KB,
webhook_idexceeds 128 characters, orevent_typecontains invalid characters. - How to fix it: Validate data before instantiation. The
field_validatordecorators enforce gateway constraints explicitly. - Code showing the fix: Wrap payload creation in a try-except block and log the specific field violation before queuing.