Efficient API Usage

Request patterns that keep Conviva API usage under the rate limits and avoid HTTP 429 responses: batch metrics, group by a dimension, cache settled windows, and choose the right API.

Updated 2026-09-02 api-best-practices, efficient-api-usage, rate-limits, 429, caching

Efficient API Usage

If some of your API calls come back as HTTP 429 (Too Many Requests), the cause is almost always the request pattern, not the amount of data you need. The same numbers are usually available with far fewer calls, using features already in the API. Two services on the same c3 account and the same rate limits can behave very differently: one runs well under the limit while the other is throttled, because one batches its requests and caches settled data and the other doesn't.

This page collects the patterns that keep you under the rate limits. The examples keep your credential off the command line (see Authentication).

Find your situation

If this sounds like your code Go to
You call a separate endpoint for each metric.Batch metrics in one call
You loop one call per title, asset, or filter value.Group by a dimension
Your dashboard re-fetches the same past hours or days on every refresh.Cache settled windows
You need sub-hour freshness, or you are hammering the historical pool.Choose the right API
You need per-viewer or per-session rows in bulk.Use Connect Data Feeds
You are getting HTTP 429 responses.Handle 429 with backoff

Understand the shared analytical pool

Historical Metrics V3 and Sessions V3 draw on one shared analytical rate limit per c3 account. Real-Time Metrics V3 has its own, higher limit. When you exceed either, the API returns HTTP 429. For the current per-endpoint values, see the Rate Limits table, which is the single source of truth.

Two consequences developers often miss:

  • Every client under your account shares the analytical budget. A single service that retries hard on 429 slows down every other service on the account.

  • A limit that was raised for your account is not the default. Build efficient patterns rather than depending on extra headroom that could change.

Batch metrics in one call

Requesting one metric at a time (sometimes called metric fan-out) multiplies your call count for no reason. The custom-selection endpoint returns up to 12 singular metrics in one request, using a repeated metric= parameter.

Before: one call per metric, three calls for the same window.

Copy
curl --netrc "https://api.conviva.com/insights/3.0/metrics/plays?days=1"
curl --netrc "https://api.conviva.com/insights/3.0/metrics/attempts?days=1"
curl --netrc "https://api.conviva.com/insights/3.0/metrics/rebuffering-ratio?days=1"

After: one call, three metrics.

Copy
curl --netrc "https://api.conviva.com/insights/3.0/metrics/custom-selection?metric=plays&metric=attempts&metric=rebuffering-ratio&days=1"

Result: Three calls become one. Only singular metrics qualify, and you can select up to 12 per request.

Group by a dimension, not per-entity loops

Looping one call per title, asset, or filter value is the most expensive common mistake. The group-by/{dimension} endpoint returns every value of a dimension in one response, and you can combine it with custom-selection to batch metrics at the same time.

Before: one call per asset.

Copy
for id in "${asset_ids[@]}"; do
  curl --netrc "https://api.conviva.com/insights/3.0/metrics/plays?days=1&filter_id=$id"
done

After: one call returns every asset, ranked.

Copy
curl --netrc "https://api.conviva.com/insights/3.0/metrics/custom-selection/group-by/asset?metric=plays&metric=rebuffering-ratio&granularity=P1D&order=desc&sort_by=plays&limit=50&days=1"

Result: One call replaces the whole loop. Grouped values arrive under time_series[].dimensional_data.

Keep three constraints in mind:

  • Keep limit at 50 or below to return many values in one response. A limit above 50 is accepted only when the time range is under 24 hours, or when granularity is ALL. Otherwise the API returns HTTP 422. See the Dimensions reference for the current per-call value cap.

  • Each call groups by a single dimension. A two-axis need (for example, per title and per device operating system) still filters one axis.

  • Sort with order and sort_by so the values you care about come first.

Cache settled windows

A window whose end is in the past is settled: its numbers are final and never change. Re-fetching a settled window is pure waste. Fetch each settled window once and serve it from a local cache. A dashboard is not a one-time export, so poll a small recent window rather than re-pulling a wide one on every refresh.

Two rules make caching work:

  • Align to clock boundaries. Round start_date and end_date to whole hours or days so the same period always produces the same request. Same URL means the same cache key. A drifting or millisecond-precise window defeats caching because every request looks new.

  • Cache only closed windows. Store a response only when its end is already in the past. Leave the current, still-open window uncached. Key the cache on the metric set, dimension, start, and end.

Note: Request an identical closed window twice and the data is identical every time. Caching it is always safe.

In production: The examples below cache settled windows forever. In a long-running service, add a time-to-live (TTL) or size limit so the cache does not grow without bound, and evict the oldest entries when it does.

Choose the right API for the freshness you need

Match the API to how fresh the data must be. Running a live dashboard on the shared historical pool is the most common reason accounts hit 429.

What you need Use Notes
Sub-hour, live freshnessReal-Time Metrics V3A separate rate limit from the analytical pool. Use it for live monitoring.
Settled historical dataHistorical Metrics V3Shared analytical pool. Cache settled windows.
Interactive per-viewer or per-session detailSessions V3Shared analytical pool. For single lookups, not bulk history.
Bulk per-viewer or per-session exportConnect Data FeedsScheduled bulk delivery, no per-call rate limit.

Bulk per-viewer or per-session data

The Sessions V3 viewer endpoint is built for interactive, per-viewer lookups, not bulk history. It has no bulk method by design, and enumerating your whole viewer population one viewer_id per call will exhaust the shared analytical pool. For bulk per-viewer or per-session history, use Connect Data Feeds (Session Feed), which delivers session-level rows on a schedule with no per-call rate limit.

Handle HTTP 429 with backoff

Fix the request pattern first: backoff is the safety net, not the fix. When you do get a 429, wait before retrying. Honor the Retry-After response header if it is present, otherwise back off exponentially (for example 1s, 2s, 4s) with a little random jitter. Retrying immediately only adds load to the same limiter and worsens throttling for every client on your account.

Put it together

These examples combine the patterns: batch metrics with custom-selection, group by a dimension, cache settled windows, and back off on 429. Both run against the live API. The same request patterns work from any language.

Python (reads your credential from ~/.netrc, so nothing sensitive is in code):

Copy
"""Efficient Conviva Metrics V3 usage: batch, group-by, cache settled windows, back off on 429.
Reads credentials from your ~/.netrc (machine api.conviva.com), so nothing sensitive is in code."""
import os, json, time, random, calendar, hashlib
import requests

BASE = "https://api.conviva.com/insights/3.0/metrics"
CACHE_DIR = ".conviva_cache"
os.makedirs(CACHE_DIR, exist_ok=True)
session = requests.Session() # requests reads ~/.netrc automatically


def api_get(path, params):
    """GET with exponential backoff + jitter, honoring Retry-After."""
    for attempt in range(5):
        r = session.get(f"{BASE}/{path}", params=params)
        if r.status_code == 200:
            return r.json()
        if r.status_code == 429:
            try:
                wait = float(r.headers.get("Retry-After", ""))
            except ValueError:
                wait = 2 ** attempt
            time.sleep(wait + random.random()) # backoff + jitter
            continue
        r.raise_for_status()
    raise RuntimeError("still rate-limited after retries")


def cached_get(path, params, end_epoch):
    """Cache any window whose end is in the past: settled data never changes."""
    key = hashlib.sha256(f"{path}?{sorted(params.items())}".encode()).hexdigest()
    cache_file = os.path.join(CACHE_DIR, key + ".json")
    settled = end_epoch < time.time()
    if settled and os.path.exists(cache_file):
        with open(cache_file) as f:
            return json.load(f)
    data = api_get(path, params)
    if settled:
        with open(cache_file, "w") as f:
            json.dump(data, f)
    return data


if __name__ == "__main__":
    # One call, three metrics (batching):
    api_get("custom-selection",
            {"metric": ["plays", "attempts", "rebuffering-ratio"], "days": 1})
    # One call, every asset (group-by):
    api_get("custom-selection/group-by/asset",
            {"metric": "plays", "granularity": "P1D",
             "order": "desc", "sort_by": "plays", "limit": 50, "days": 1})
    # A settled window, cached after the first fetch:
    start, end = "2026-09-01T00:00:00Z", "2026-09-02T00:00:00Z"
    end_epoch = calendar.timegm(time.strptime(end, "%Y-%m-%dT%H:%M:%SZ"))
    cached_get("plays", {"start_date": start, "end_date": end}, end_epoch)
    cached_get("plays", {"start_date": start, "end_date": end}, end_epoch) # cache hit
    print("ok: batch, group-by, and cached settled window all returned")

JavaScript (Node.js; reads your credential from environment variables):

Copy
// Efficient Conviva Metrics V3 usage: batch, group-by, cache settled windows, back off on 429.
// Reads credentials from environment variables, so nothing sensitive is in code:
// export CONVIVA_CLIENT_ID=... export CONVIVA_CLIENT_SECRET=...
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";

const BASE = "https://api.conviva.com/insights/3.0/metrics";
const CACHE_DIR = ".conviva_cache";
fs.mkdirSync(CACHE_DIR, { recursive: true });
const AUTH = "Basic " + Buffer.from(
  `${process.env.CONVIVA_CLIENT_ID}:${process.env.CONVIVA_CLIENT_SECRET}`).toString("base64");

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// Repeat a parameter (metric=a&metric=b) when its value is an array.
// Sort by key so the same request always produces the same string (a stable cache key).
function qs(params) {
  const sp = new URLSearchParams();
  for (const [k, v] of Object.entries(params).sort(([a], [b]) => a.localeCompare(b))) {
    if (Array.isArray(v)) v.forEach((x) => sp.append(k, x));
    else sp.append(k, v);
  }
  return sp.toString();
}

// GET with exponential backoff + jitter, honoring Retry-After.
async function apiGet(pathName, params) {
  const url = `${BASE}/${pathName}?${qs(params)}`;
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(url, { headers: { Authorization: AUTH } });
    if (res.status === 200) return res.json();
    if (res.status === 429) {
      const wait = Number(res.headers.get("retry-after")) || 2 ** attempt;
      await sleep((wait + Math.random()) * 1000); // backoff + jitter
      continue;
    }
    throw new Error(`request failed: HTTP ${res.status}`);
  }
  throw new Error("still rate-limited after retries");
}

// Cache any window whose end is in the past: settled data never changes.
async function cachedGet(pathName, params, endEpoch) {
  const key = crypto.createHash("sha256").update(`${pathName}?${qs(params)}`).digest("hex");
  const file = path.join(CACHE_DIR, `${key}.json`);
  const settled = endEpoch < Date.now() / 1000;
  if (settled && fs.existsSync(file)) return JSON.parse(fs.readFileSync(file));
  const data = await apiGet(pathName, params);
  if (settled) fs.writeFileSync(file, JSON.stringify(data));
  return data;
}

// One call, three metrics (batching):
await apiGet("custom-selection", { metric: ["plays", "attempts", "rebuffering-ratio"], days: 1 });
// One call, every asset (group-by):
await apiGet("custom-selection/group-by/asset",
  { metric: "plays", granularity: "P1D", order: "desc", sort_by: "plays", limit: 50, days: 1 });
// A settled window, cached after the first fetch:
const start = "2026-09-01T00:00:00Z", end = "2026-09-02T00:00:00Z";
const endEpoch = Date.parse(end) / 1000;
await cachedGet("plays", { start_date: start, end_date: end }, endEpoch);
await cachedGet("plays", { start_date: start, end_date: end }, endEpoch); // cache hit
console.log("ok: batch, group-by, and cached settled window all returned");

What this adds up to

Change Effort Benefit
Batch metrics with custom-selectionLowUp to 12 metrics in one call instead of one call each.
Group by a dimensionLowOne call returns the whole dimension instead of one call per entity.
Cache settled windowsMediumEliminates repeat calls for closed windows.
Choose the right APILowMoves live dashboards off the shared analytical pool.
Back off on 429LowProtects every client on your account.

Quick checklist

  • Request several metrics in one custom-selection call, not one call per metric.

  • Use group-by/{dimension} instead of looping one call per entity, and keep limit at 50 or below.

  • Align time windows to clock boundaries and cache any window whose end is in the past.

  • Use Real-Time Metrics V3 for sub-hour freshness, and cache the historical pool for settled data.

  • Use Connect Data Feeds for bulk per-viewer or per-session history, not a per-viewer loop.

  • Back off with jitter on 429, and honor Retry-After.