For complex or multi-topic queries, send separate focused requests:
// Instead of one massive query, break it down:{ "query": "Competitors of company ABC." }{ "query": "Financial performance of company ABC." }{ "query": "Recent developments of company ABC." }
When latency is absolutely crucial. Delivers near-instant results, prioritizing speed over relevance. Ideal for real-time applications where response time is critical.
fast
When latency is more important than relevance, but you want results in reranked chunks format. Good for applications that need quick, targeted snippets.
basic
A solid balance between relevance and latency. Best for general-purpose searches where you need quality results without the overhead of advanced processing.
advanced
When you need the highest relevance and are willing to trade off latency. Best for queries seeking specific, detailed information.
Use exact_match only when searching for a specific name or phrase that must appear verbatim in the source content. Wrap the phrase in quotes within your query:
{ "query": "\"John Smith\" CEO Acme Corp", "exact_match": true}
Because this narrows retrieval, it may return fewer results or empty result fields when no exact matches are found. Best suited for:
Due diligence — finding information on a specific person or entity
Data enrichment — retrieving details about a known company or individual
Legal/compliance — locating exact names or phrases in public records
For larger batches, cap in-flight requests to stay under your rate limit, and tag each result so one failure (e.g. a 429) doesn’t sink the whole batch.
import asynciosem = asyncio.Semaphore(20) # in-flight cap; keep under your RPMasync def search_one(q, **kw): async with sem: for attempt in range(5): try: return {"query": q, "ok": True, "data": await client.search(q, **kw)} except Exception as e: if attempt == 4: return {"query": q, "ok": False, "error": str(e)} await asyncio.sleep(2 ** attempt) # exponential backoffasync def batch_search(queries, **kw): return await asyncio.gather(*(search_one(q, **kw) for q in queries))results = asyncio.run(batch_search(queries, search_depth="advanced"))
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));// reuses `client` and `queries` from aboveasync function searchOne(query, options = {}) { for (let attempt = 0; attempt < 5; attempt++) { try { return { query, ok: true, data: await client.search(query, options) }; } catch (e) { if (attempt === 4) return { query, ok: false, error: String(e) }; await sleep(2 ** attempt * 1000); // exponential backoff } }}// process in waves of `concurrency` to cap in-flight requestsasync function batchSearch(queries, options = {}, concurrency = 20) { const out = []; for (let i = 0; i < queries.length; i += concurrency) { const wave = queries.slice(i, i + concurrency); out.push(...(await Promise.all(wave.map((q) => searchOne(q, options))))); } return out;}const results = await batchSearch(queries, { searchDepth: "advanced" });
Size concurrency from your own rate limit: concurrency ≈ (RPM / 60) × avg_latency_s. For example, at 100 RPM and 3s avg latency that’s (100 / 60) × 3 ≈ 5. Start there and tune up while watching your 429 rate.
Dedupe the results to save tokens and avoid repetitive context — join unique content chunks with Tavily’s [...] separator.
def dedupe(results): merged = {} for r in results: if not r["ok"]: continue for item in r["data"]["results"]: url = item["url"].split("?")[0].rstrip("/") # canonicalize e = merged.setdefault(url, {"url": url, "score": 0, "chunks": []}) e["score"] = max(e["score"], item.get("score", 0)) for c in item.get("content", "").split("[...]"): if (c := c.strip()) and c not in e["chunks"]: e["chunks"].append(c) return sorted( ({"url": e["url"], "score": e["score"], "content": " [...] ".join(e["chunks"])} for e in merged.values()), key=lambda x: x["score"], reverse=True, )corpus = dedupe(results) # dedupe the batch results from above
function dedupe(results) { const merged = new Map(); for (const r of results) { if (!r.ok) continue; for (const item of r.data.results) { const url = item.url.split("?")[0].replace(/\/+$/, ""); // canonicalize const e = merged.get(url) || { url, score: 0, chunks: [] }; e.score = Math.max(e.score, item.score ?? 0); for (const c of (item.content || "").split("[...]")) { const t = c.trim(); if (t && !e.chunks.includes(t)) e.chunks.push(t); } merged.set(url, e); } } return [...merged.values()] .map((e) => ({ url: e.url, score: e.score, content: e.chunks.join(" [...] ") })) .sort((a, b) => b.score - a.score);}const corpus = dedupe(results); // dedupe the batch results from above
When an agent issues several Tavily calls to answer a single user task — for example, retrieving sources, then extracting full content from a subset, then running follow-up searches — pass a consistent session_id across all related calls.If your agent serves multiple end-users behind a single API key, also pass a stable human_id per user. For security, Tavily hashes human IDs before processing or storing them.See the SDK references or the API HTTP headers for how to set these.