Skip to content

Batching requests

The single most effective optimization on oneapi.finance is batched requests. A /v1/quote?symbols=A,B,C,D,E,F,G,H call costs one token and returns eight quotes. The naive equivalent (eight separate calls) costs eight tokens.

This recipe covers the endpoints that support batching, the right batch size, and parallelism patterns.

Endpoints that support batching

EndpointBatch paramMax per call
/v1/quotesymbols=A,B,C8
/v1/dividendssymbols=A,B,C8
/v1/fx/time_seriespairs=USD/EUR,GBP/JPY10

Endpoints that do not batch (/v1/time_series, /v1/statistics, /v1/profile, /v1/splits, /v1/symbol_search) take a single symbol per call and you should parallelize them client-side.

Why eight

Internally, batches are decomposed and dispatched to upstream sources in parallel. The cache layer is keyed per-symbol, so an 8-symbol batch with 7 cache hits and 1 miss only ever fetches one symbol from upstream. The 8-symbol ceiling exists because:

  1. Beyond ~8, the marginal benefit of avoiding HTTP overhead falls off relative to the cost of holding one HTTP connection open longer.
  2. Fan-out concurrency on the upstream side is bounded by source rate limits; beyond 8, we would be queuing inside our gateway, which is just hiding latency from you.

For 9+ symbols, parallelize 8-symbol batches client-side.

Batch + parallel fan-out

import httpx
import asyncio
API_KEY = "oa_live_..."
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
CHUNK = 8
MAX_CONCURRENCY = 4 # 4 batches × 8 symbols = 32 symbols/wave
def chunked(it, n):
out = []
for x in it:
out.append(x)
if len(out) == n:
yield out
out = []
if out:
yield out
async def quotes_async(symbols: list[str]) -> dict[str, dict]:
sem = asyncio.Semaphore(MAX_CONCURRENCY)
async with httpx.AsyncClient(headers=HEADERS, timeout=15.0) as client:
async def one_batch(batch: list[str]) -> dict:
async with sem:
r = await client.get(
"https://api.oneapi.finance/v1/quote",
params={"symbols": ",".join(batch)},
)
r.raise_for_status()
return r.json().get("quotes", {})
results = await asyncio.gather(*[one_batch(b) for b in chunked(symbols, CHUNK)])
out = {}
for d in results:
out.update(d)
return out
# 100 symbols → ceil(100/8) = 13 batched calls, max 4 in flight.

The same logic in JS:

const CHUNK = 8;
const MAX_CONCURRENCY = 4;
async function quotes(symbols) {
const batches = [];
for (let i = 0; i < symbols.length; i += CHUNK) {
batches.push(symbols.slice(i, i + CHUNK));
}
const out = {};
for (let i = 0; i < batches.length; i += MAX_CONCURRENCY) {
const wave = batches.slice(i, i + MAX_CONCURRENCY);
const results = await Promise.all(
wave.map(async (batch) => {
const url = `https://api.oneapi.finance/v1/quote?symbols=${batch.join(",")}`;
const r = await fetch(url, { headers: { Authorization: `Bearer ${process.env.ONEAPI_KEY}` } });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const body = await r.json();
return body.quotes ?? {};
}),
);
for (const d of results) Object.assign(out, d);
}
return out;
}

Quota math

For 100 symbols hit every minute on the Indie tier:

StrategyCalls per minuteCalls per dayDays to exhaust 100k quota
Single, sequential100144,0000.7
Single, parallel (10 concurrent)100144,0000.7
Batched (8/call), sequential1318,720~5.3
Batched (8/call), parallel (4 concurrent)1318,720~5.3
Batched + cached at 60s TTL on the client1-13 (depending on hit rate)under 18,720many

The combination of batching and caching is what makes the Indie tier viable for non-trivial dashboards.

Handling per-symbol failures

For batched /v1/quote and /v1/dividends, individual symbol errors are not propagated as a non-2xx status. Instead, the failed symbols are listed in data_issues:

{
"quotes": { "AAPL": { "...": "..." } },
"data_issues": ["XYZQ: not_found"]
}

Always check data_issues so you can mark those symbols invalid in your UI and stop requesting them.

When not to batch

  • When you need a different parameter per call (e.g. different outputsize or interval). Time-series queries are single-symbol for this reason.
  • When latency for the first symbol matters more than total throughput. A batched request waits for the slowest upstream in the batch before responding.

See also