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
429slows 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.
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.
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.
for id in "${asset_ids[@]}"; do
curl --netrc "https://api.conviva.com/insights/3.0/metrics/plays?days=1&filter_id=$id"
doneAfter: one call returns every asset, ranked.
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
limitat 50 or below to return many values in one response. Alimitabove 50 is accepted only when the time range is under 24 hours, or whengranularityisALL. Otherwise the API returns HTTP422. 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
orderandsort_byso 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_dateandend_dateto 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 freshness | Real-Time Metrics V3 | A separate rate limit from the analytical pool. Use it for live monitoring. |
| Settled historical data | Historical Metrics V3 | Shared analytical pool. Cache settled windows. |
| Interactive per-viewer or per-session detail | Sessions V3 | Shared analytical pool. For single lookups, not bulk history. |
| Bulk per-viewer or per-session export | Connect Data Feeds | Scheduled 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):
"""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):
// 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-selection | Low | Up to 12 metrics in one call instead of one call each. |
| Group by a dimension | Low | One call returns the whole dimension instead of one call per entity. |
| Cache settled windows | Medium | Eliminates repeat calls for closed windows. |
| Choose the right API | Low | Moves live dashboards off the shared analytical pool. |
Back off on 429 | Low | Protects every client on your account. |
Quick checklist
Request several metrics in one
custom-selectioncall, not one call per metric.Use
group-by/{dimension}instead of looping one call per entity, and keeplimitat 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 honorRetry-After.