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-25 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
You think you must choose between many metrics and grouping by a dimension.Pull multiple metrics and group by together
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.

Discover the metrics and dimensions you can query

Start from the catalog of what your account supports. A single call to GET /insights/3.0/metrics returns every metric name (in metrics.singular) and every group-by dimension name (in dimensions), in the exact kebab-case form the request paths expect. Read these names from the catalog rather than hard-coding them, so your integration keeps using valid, current names as the available metrics and dimensions evolve.

Copy
curl --netrc "https://api.conviva.com/insights/3.0/metrics"

Result: One response lists the metric and dimension names to use in every other request. Fetch it once, cache it, and refresh periodically. See Retrieve List of Available Metrics.

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 return several metrics at once. You do not have to choose between the two. For the full multi-metric pattern, see Pull multiple metrics and group by together.

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 24 hours or less, 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.

Pull multiple metrics and group by together

A common misconception is that custom-selection (many metrics) and group-by (a breakdown by dimension) are mutually exclusive. They are not. Combine them in one request to return several metrics for every value of a dimension: call custom-selection/group-by/{dimension} with a repeated metric= parameter.

One call: twelve metrics, broken down by country, for a saved filter.

Copy
curl --netrc "https://api.conviva.com/insights/3.0/metrics/custom-selection/group-by/geo-country-code?start_date=2026-09-22T19:00:00Z&end_date=2026-09-22T20:00:00Z&filter_id={filter_id}&metric=attempts&metric=bitrate&metric=video-start-failures&metric=exits-before-video-start&metric=plays&metric=video-start-time&metric=concurrent-plays&metric=rebuffering-ratio&metric=video-playback-failures&metric=ended-plays&metric=connection-induced-rebuffering-ratio&metric=video-restart-time"

Result: One response carries all requested metrics for every country. Each dimension value arrives as an entry under time_series[].dimensional_data[], with that value's numbers under its own metrics object. For the parameter details, see the reference guide's Combining Multiple Metrics with Group-By.

The same limits as a plain group-by apply:

  • Up to 12 singular metrics per request, each passed as its own metric= parameter.

  • One group-by dimension per call. Narrow any other axis with a dimensional filter or a saved filter_id.

  • Rank with sort_by (a single metric) and order, and keep limit at 50 or below unless the time range is 24 hours or less, or granularity is ALL. Otherwise the API returns HTTP 422.

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.
Combine custom-selection with group-byLowMany metrics, every dimension value, in a single call.
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

  • Discover the supported metric and dimension names from the catalog (GET /insights/3.0/metrics) rather than hard-coding them. See Retrieve List of Available Metrics.

  • 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.

  • Combine custom-selection with group-by to pull several metrics and a dimension breakdown in one call.

  • 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.