Skip to content
Last updated

API Rate Limiting & Throttling

MediaMath APIs implement rate limiting and throttling to ensure platform stability and fair resource allocation across all users. This page explains the limits, how they work, and how to handle rate-limited responses in your integrations.

Overview

The MediaMath API uses a multi-layer protection system to maintain service reliability. When limits are exceeded, the API returns appropriate HTTP status codes with details to help you adjust your request patterns.

Rate Limits

Limit TypeSafe LimitTheoretical MaximumDescription
Per-User Rate Limit40 req/s50 req/sSustained request rate per authenticated user
Per-User Burst Capacity200 requests250 requestsMaximum instant burst after idle period
Concurrent Write Limit120 requests150 requestsMaximum concurrent POST/PUT/PATCH/DELETE requests
Per-IP Rate Limit80 req/s100 req/sSustained request rate per IP (unauthenticated user)

Understanding Limits: The Safe Limit accounts for load balancing distribution variance and preserves headroom below the hard per-pod enforcement capacity. Theoretical Maximum assumes perfect round-robin distribution across production pods. Error responses show per-pod enforcement values (Per-User: 10 req/s, Burst: 50, Concurrent: 30, Per-IP: 20 req/s).

How Burst Capacity Works:

  • Uses token bucket algorithm with continuous refill (not fixed time windows)
  • Each request consumes 1 token and tokens refill continuously across the cluster
  • After idle period ≥5 seconds, safe burst capacity is available (200 tokens cluster-wide)
  • Example: After being idle, you can send up to 200 requests instantly, then continue at sustained rate of 40 req/s
  • Sustained rate at or below the limit never depletes burst capacity
  • The API runs on multiple pods with load balancing, distributing your requests across the cluster

Error Responses

HTTP 429 Too Many Requests

Returned when rate limit or concurrent write limit is exceeded.

Rate Limit Exceeded:

{
  "meta": {
    "status": "error",
    "uuid": "request-uuid"
  },
  "errors": [{
    "code": "rate-limit-exceeded",
    "message": "Rate limit exceeded, please slow down",
    "details": {
      "limit": 10,
      "burst": 50,
      "window": "1s"
    }
  }]
}

Note: Error responses show per-pod enforcement values (10 req/s sustained, 50 burst capacity). Due to load balancing across production pods, your effective capacity is higher. See the Rate Limits table above for Safe Limit and Theoretical Maximum.

Concurrent Write Limit Exceeded:

{
  "meta": {
    "status": "error",
    "uuid": "request-uuid"
  },
  "errors": [{
    "code": "too-many-concurrent-writes",
    "message": "Too many concurrent write operations, please retry",
    "details": {
      "limit": 30
    }
  }]
}

Note: Error responses show the per-pod enforcement limit (30 concurrent writes). The 150-request theoretical maximum assumes perfect distribution across production pods, while the 120-request safe limit preserves load-balancing headroom.

Response Headers:

The API returns informational headers to help you manage request patterns:

Rate Limit Headers (when the request is rate-limit enforced):

  • X-RateLimit-Limit: 10 - Per-pod rate limit in requests per second
  • X-RateLimit-Remaining: 3 - Current number of tokens available in your bucket on this pod
  • X-RateLimit-Reset: 1638360100 - Unix timestamp when the next token becomes available. On rate-limit 429 responses, this is based on the same rounded-up token-bucket refill delay as Retry-After. It is also returned when a rate-limit-enforced request is allowed by the limiter and remaining tokens reach 0
  • Retry-After: 1 - Seconds to wait before retrying (on rate-limit 429 errors, rounded up from the token-bucket refill delay)

Concurrency Limit Headers (on write operation 429 responses only):

  • Retry-After: 1 - Suggested wait time in seconds before retrying (includes jitter to prevent thundering herd around a 1-second base)

Note: Headers show per-pod values. The effective cluster-wide capacity is higher due to load balancing. See the Rate Limits table above for Safe Limit and Theoretical Maximum.

HTTP 503 Service Unavailable

Returned when the API is under heavy load or experiencing issues.

{
  "meta": {
    "status": "error",
    "uuid": "request-uuid"
  },
  "errors": [{
    "code": "service-overloaded",
    "message": "Service temporarily overloaded, please retry later"
  }]
}

Headers returned:

  • Retry-After: 5 or Retry-After: 10 - Suggested wait time in seconds (includes jitter to prevent thundering herd)

Client Behavior

  1. Implement Exponential Backoff:
  • On 429 or 503, wait for Retry-After header value
  • If retrying fails, double the wait time (max 60 seconds)
  • Example: 1s → 2s → 4s → 8s → 16s → 32s → 60s
  1. Respect Rate Limits:
  • Space out requests to stay under 40 req/s per user
  • Batch operations when possible instead of many small requests
  1. Handle Concurrent Write Limits:
  • For bulk updates, submit requests sequentially or in small batches
  • Keep simultaneous write requests below 120
  1. Monitor Response Headers:
  • X-RateLimit-Limit: Per-pod rate limit in requests per second
  • X-RateLimit-Remaining: Current tokens available in your bucket
  • X-RateLimit-Reset: Unix timestamp when next token becomes available. For rate-limit 429 responses, this uses the same rounded-up token-bucket refill delay as Retry-After
  • Retry-After: Seconds to wait before retrying on 429/503 errors. Rate-limit 429 values are rounded up from the token-bucket refill delay, while concurrency and overload values include jitter to prevent thundering herd

Code Examples

JavaScript (with fetch)

async function apiRequestWithRetry(url, token, options = {}, maxRetries = 5) {
  let lastError;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, {
        ...options,
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json',
          ...options.headers,
        },
      });

      // Success - return response
      if (response.ok) {
        return response;
      }

      // Rate limited or service unavailable - retry with backoff
      if (response.status === 429 || response.status === 503) {
        const retryAfter = Number.parseInt(response.headers.get('Retry-After') || '1', 10);
        const retryAfterSeconds = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : 1;
        const waitTime = Math.min(retryAfterSeconds * 1000 * Math.pow(2, attempt), 60000);

        console.log(`Rate limited. Waiting ${waitTime}ms before retry ${attempt + 1}/${maxRetries}`);
        await new Promise(resolve => setTimeout(resolve, waitTime));
        continue;
      }

      // Other error - don't retry
      return response;

    } catch (error) {
      lastError = error;
      // Network error - retry with backoff
      const waitTime = Math.min(1000 * Math.pow(2, attempt), 60000);
      await new Promise(resolve => setTimeout(resolve, waitTime));
    }
  }

  throw lastError || new Error('Max retries exceeded');
}

// Usage
const response = await apiRequestWithRetry(
  'https://api.mediamath.com/api/v3.0/campaigns',
  'your-api-token'
);
const data = await response.json();

Python (with requests)

import time
import requests
from typing import Optional, Dict, Any

def api_request_with_retry(
    url: str,
    method: str = 'GET',
    headers: Optional[Dict[str, str]] = None,
    data: Optional[Dict[str, Any]] = None,
    max_retries: int = 5,
    token: Optional[str] = None
) -> requests.Response:
    """Make API request with automatic retry on rate limit errors."""

    request_headers = {
        'Content-Type': 'application/json',
    }
    if token:
        request_headers['Authorization'] = f'Bearer {token}'
    if headers:
        request_headers.update(headers)

    last_error = None

    for attempt in range(max_retries):
        try:
            response = requests.request(
                method=method,
                url=url,
                headers=request_headers,
                json=data,
                timeout=30
            )

            # Success
            if response.ok:
                return response

            # Rate limited or service unavailable - retry with backoff
            if response.status_code in (429, 503):
                try:
                    retry_after = max(int(response.headers.get('Retry-After', 1)), 1)
                except (TypeError, ValueError):
                    retry_after = 1
                wait_time = min(retry_after * (2 ** attempt), 60)

                print(f"Rate limited. Waiting {wait_time}s before retry {attempt + 1}/{max_retries}")
                time.sleep(wait_time)
                continue

            # Other error - don't retry
            return response

        except requests.exceptions.RequestException as e:
            last_error = e
            wait_time = min(1 * (2 ** attempt), 60)
            time.sleep(wait_time)

    if last_error:
        raise last_error
    raise Exception('Max retries exceeded')


# Usage
response = api_request_with_retry(
    url='https://api.mediamath.com/api/v3.0/campaigns',
    method='GET',
    token='your-api-token'
)
data = response.json()

Batch Processing Example (JavaScript)

async function processBatchWithRateLimit(items, processFn, batchSize = 10, delayMs = 1000) {
  if (batchSize <= 0) {
    throw new Error('batchSize must be greater than 0');
  }

  const results = [];

  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);

    // Process only one bounded batch concurrently
    const batchResults = await Promise.all(
      batch.map(item => processFn(item))
    );

    results.push(...batchResults);

    // Wait between batches to stay under rate limits and smooth load
    if (i + batchSize < items.length) {
      await new Promise(resolve => setTimeout(resolve, delayMs));
    }
  }

  return results;
}

// Usage: update campaigns in batches of 10 instead of sending all requests at once
const token = 'your-api-token';
const campaigns = [
  { id: 123, name: 'Campaign 123 update' },
  { id: 456, name: 'Campaign 456 update' },
];

const results = await processBatchWithRateLimit(
  campaigns,
  (campaign) => apiRequestWithRetry(
    `https://api.mediamath.com/api/v3.0/campaigns/${campaign.id}`,
    token,
    {
      method: 'POST',
      body: JSON.stringify({ name: campaign.name }),
    }
  ),
  10,   // batch size: at most 10 concurrent requests
  1000  // 1 second delay between batches
);

Batch Processing Example (Python)

import time
import requests
from concurrent.futures import ThreadPoolExecutor
from typing import Any, Callable, Dict, Iterable, List, TypeVar

T = TypeVar('T')
R = TypeVar('R')

def process_batch_with_rate_limit(
    items: Iterable[T],
    process_fn: Callable[[T], R],
    batch_size: int = 10,
    delay_seconds: float = 1.0
) -> List[R]:
    """Process items in bounded batches instead of sending all requests at once."""

    if batch_size <= 0:
        raise ValueError('batch_size must be greater than 0')

    item_list = list(items)
    results: List[R] = []

    for start in range(0, len(item_list), batch_size):
        batch = item_list[start:start + batch_size]

        # Process only one bounded batch concurrently
        with ThreadPoolExecutor(max_workers=batch_size) as executor:
            results.extend(executor.map(process_fn, batch))

        # Wait between batches to stay under rate limits and smooth load
        if start + batch_size < len(item_list):
            time.sleep(delay_seconds)

    return results


# Usage: update campaigns in batches of 10 instead of sending all requests at once
token = 'your-api-token'
campaigns = [
    {'id': 123, 'name': 'Campaign 123 update'},
    {'id': 456, 'name': 'Campaign 456 update'},
]

def update_campaign(campaign: Dict[str, Any]) -> requests.Response:
    return api_request_with_retry(
        url=f'https://api.mediamath.com/api/v3.0/campaigns/{campaign["id"]}',
        method='POST',
        data={'name': campaign['name']},
        token=token
    )

results = process_batch_with_rate_limit(
    campaigns,
    update_campaign,
    batch_size=10,
    delay_seconds=1.0
)

FAQ

Q: Why do the documented limits differ from the error response values?

The documented limits show safe cluster-wide limits accounting for load balancing across production pods. Error responses show per-pod enforcement values because each individual pod enforces limits independently. Your effective capacity is higher than per-pod values due to request distribution, but lower than the hard theoretical maximum to preserve headroom.

Example:

  • Error shows: "limit": 10 (per-pod)
  • Effective capacity: 40 req/s safe limit, 50 req/s theoretical max
  • Safe limit uses 80% safety margin to account for distribution variance

Q: Why am I getting rate limited at low request volumes?

Rate limits are per-user, not per-application. If you have multiple applications or scripts using the same API credentials, they share the same rate limit bucket.

Q: How do I request a higher rate limit?

Contact MediaMath Support to discuss your use case. Higher limits may be available for specific approved integrations.

Q: Are rate limits applied to read and write operations equally?

Rate limits (429 responses from rate limiting) apply to all requests equally. However, concurrent write limits (429 responses from concurrency limiting) only apply to POST, PUT, PATCH, and DELETE requests.

Q: What timezone are rate limit windows based on?

Rate limit windows are rolling and continuous (token bucket algorithm), not calendar-based windows. Each request is evaluated against your current token balance, which refills continuously. The cluster-wide effective rate is approximately 40 tokens per second (distributed across multiple pods).

Q: What should I do if I consistently hit rate limits?

If you're frequently receiving 429 responses:

  1. Implement request batching where possible to reduce total request count
  2. Add delays between requests (target 20-30 req/s sustained rate)
  3. Use exponential backoff retry logic as shown in the code examples above
  4. Review your integration to eliminate unnecessary API calls
  5. Contact MediaMath Support if you have legitimate high-volume needs that cannot be optimized

Q: Is there a way to check my current rate limit status?

Yes. Monitor the X-RateLimit-Remaining header in API responses to see your current token balance. This shows how many requests you can make before hitting the rate limit. When the bucket is exhausted or a request is rejected by the rate limiter, the X-RateLimit-Reset header provides a Unix timestamp indicating when your next token becomes available, using the same rounded-up refill delay as Retry-After on 429 responses.

Q: Why am I getting 503 errors instead of 429?

HTTP 503 Service Unavailable indicates system-level overload (not user-specific rate limiting). This is a temporary condition caused by high overall system load. When you receive 503 errors, implement exponential backoff and retry - the system will recover automatically. The Retry-After header indicates how long to wait.