Aggregating Data from Multiple APIs: Patterns and Pitfalls
Master the art of aggregating data from multiple external APIs with proper error handling, rate limiting, caching, and response normalization strategies.
Introduction
Modern applications rarely exist in isolation. A typical dashboard might pull user data from your authentication provider, metrics from your analytics platform, inventory from your ERP system, and notifications from a third-party messaging service. Each API has its own quirks: different authentication schemes, inconsistent response formats, varying rate limits, and unpredictable availability windows.
This is the multi-API aggregation challenge: how do you combine data from disparate sources into a unified, reliable response for your application? The naive approach of sequential HTTP calls quickly falls apart. One slow API blocks the entire response. One failed service crashes your page. Rate limits get exhausted because you’re making redundant requests.
In this article, we’ll explore battle-tested patterns for aggregating data from multiple APIs using Python’s async capabilities. We’ll cover concurrent fetching, rate limiting, response normalization, caching strategies, and resilience patterns like circuit breakers. By the end, you’ll have a toolkit for building aggregation services that are fast, reliable, and maintainable.
The challenges of multi-API aggregation
Before diving into solutions, let’s understand what makes API aggregation genuinely difficult:
Rate limiting across providers
Every API has rate limits, but they’re rarely consistent:
Provider
Rate Limit
Window
Penalty
GitHub API
5,000 req
per hour
403 until reset
Stripe API
100 req
per second
429 with retry-after
OpenAI API
Varies by tier
per minute
429 with exponential backoff
Twitter/X API
15 req
per 15 min
429, 15-min lockout
Aggregating across these providers means tracking multiple rate limit budgets simultaneously and gracefully degrading when any one is exhausted.
Format inconsistency
APIs speak different dialects. One returns created_at as a Unix timestamp, another as ISO 8601, a third as a human-readable string. User IDs might be integers, UUIDs, or opaque strings. Pagination uses cursors in one API and page numbers in another.
Availability variance
External APIs fail. They fail in different ways (timeouts, 5xx errors, malformed responses), at different rates, and with different recovery patterns. Your aggregation layer must handle partial failures gracefully, returning what data is available rather than failing entirely.
Async fetching with httpx
Python’s asyncio combined with httpx provides the foundation for efficient concurrent API calls. Instead of waiting for each API sequentially, we can fire requests in parallel and gather results.
async_fetcher.py
import asyncio
import httpx
from typing import Any
from dataclasses import dataclass
@dataclass
classAPIResponse:
source: str
data: dict[str, Any] | None
error: str | None
status_code: int | None
asyncdeffetch_api(
client: httpx.AsyncClient,
name: str,
url: str,
headers: dict[str, str] | None = None,
timeout: float = 10.0
) -> APIResponse:
"""Fetch from a single API with error handling."""
return {r.source: r.data for r in results if r.data}
The key insight here is that asyncio.gather() runs all requests concurrently. If your three APIs have response times of 200ms, 350ms, and 150ms, the total time is approximately 350ms (the slowest), not 700ms (the sum).
Pro Tip: Use asyncio.gather(*tasks, return_exceptions=True) to prevent one failed task from canceling others. This is essential when you want partial results.
Implementing rate limiting
A robust aggregation service needs rate limiting that works across all your API consumers. Here’s a token bucket implementation that handles multiple API rate limits:
This implementation ensures you never exceed the rate limits of any provider, automatically queuing requests when limits are approached.
Warning: In distributed systems, a single-process rate limiter isn’t sufficient. Use Redis-based rate limiting (covered in the caching section) for production deployments across multiple workers.
Response normalization
Raw API responses are inconsistent by nature. A normalization layer transforms diverse formats into a consistent internal schema:
"""Normalize a response using the appropriate normalizer."""
normalizer = NORMALIZERS.get(source)
if not normalizer:
raiseValueError(f"No normalizer for source: {source}")
return normalizer.normalize(raw_data)
The Protocol-based approach allows easy testing and extension. When you add a new API, you just implement the normalizer interface and register it.
Caching strategies with Redis
API responses often don’t change frequently. Caching reduces latency, decreases API costs, and provides a fallback when external services are unavailable:
cache.py
import json
import hashlib
from typing import Any, Callable, Awaitable, TypeVar
The stale-while-revalidate pattern is particularly powerful for aggregation services. When your cache expires but the external API is down, you can serve stale data rather than failing entirely.
Note: Set different TTLs based on data volatility. User profiles might cache for hours, while stock prices should cache for seconds.
Circuit breaker pattern
When an external API fails repeatedly, continuing to call it wastes resources and slows down your aggregation. The circuit breaker pattern provides automatic protection:
circuit_breaker.py
import asyncio
import time
from enum import Enum
from dataclasses import dataclass, field
from typing import Callable, Awaitable, TypeVar
T = TypeVar('T')
classCircuitState(Enum):
CLOSED = "closed"# Normal operation
OPEN = "open"# Failing, reject requests
HALF_OPEN = "half_open"# Testing if service recovered
Integrate this with your observability stack (Prometheus, Datadog) to track trends and alert on sustained degradation.
Conclusion
Building a robust API aggregation layer requires addressing multiple concerns simultaneously: concurrency for performance, rate limiting for compliance, normalization for consistency, caching for efficiency, circuit breakers for resilience, and monitoring for visibility.
Key takeaways:
Use async/await with httpx or aiohttp for concurrent fetching - sequential calls don’t scale
Implement per-API rate limiting using token bucket or leaky bucket algorithms
Normalize responses early to isolate format differences from business logic
Cache aggressively with stale-while-revalidate for resilience during outages
Deploy circuit breakers to fail fast and protect against cascading failures
Monitor everything - you can’t fix what you can’t see
The patterns in this article form the foundation of any serious data aggregation service. Start with the basics (async fetching, error handling), then layer on caching and circuit breakers as your reliability requirements grow. The goal isn’t to eliminate failures - external APIs will always fail eventually - but to degrade gracefully and recover automatically.