# How do I add a backup for Tavily Extract?

Keep Tavily for search and answers, and send UpRock the pages Extract doesn't cover. The URLs that land in Extract's failed results, and any page you need as it appears in one country, go to UpRock, which loads each one on a real device in the country you pick for $0.50 per 1,000 pages.

Updated 24 Oct 2026. Web page: https://uprock.ai/compare/tavily

## On this page

- [What Tavily does well](#what-tavily-does-well)
- [Two gaps a second tool covers](#two-gaps-a-second-tool-covers)
- [Tavily and UpRock side by side](#tavily-and-uprock-side-by-side)
- [Retry Tavily's failed extracts with UpRock](#retry-tavilys-failed-extracts-with-uprock)
- [FAQ](#faq)
- [Other use cases to explore](#explore)
- [Related articles](#related)
- [References](#references)
- [Get started](#get-started)

## What Tavily does well

Tavily is a search API built for LLM agents. One call returns ranked results with clean content instead of raw HTML, and it can add an LLM-written answer (`include_answer`) and each page's full text (`include_raw_content`). You can narrow a search by topic (general, news, finance), by date range, and by up to 300 domains to include or 150 to exclude. Beyond search it has Extract, Crawl, Map and a Research endpoint that writes cited reports.

It's also the default search tool in a lot of agent stacks. LangChain calls Tavily its recommended search tool, the docs list integrations with around 40 frameworks and platforms, and there's an official remote MCP server. If your agent already searches with Tavily, keep it.

## Two gaps a second tool covers

### Extract's failed results

Tavily Extract takes up to 20 URLs per call. It doesn't throw when a page can't be read: the response is HTTP 200, the readable pages are in `results`, and the rest are in `failedResults` (`failed_results` in Python) with an error string. Tavily's own docs note that a 200 can have an empty `results` array when every URL failed.

Those failed URLs are the ones to send to UpRock. They're also free on Tavily's side, so the fallback only costs the UpRock fetch.

### A page as it looks in one country

Tavily's `country` parameter boosts search results from the country you name (for general-topic searches; the list has 166 countries). It doesn't change where Extract reads a page from, and Extract has no country option of its own. For a page whose prices, stock or content change by country, that matters: you get one version, and the docs don't say from where.

UpRock's `crawl_fetch` takes the country per page (63 countries, or a region such as EU or APAC) and loads the page on a device there. On 19 Oct 2026 we checked that by fetching an IP lookup page with the country set to DE, JP and BR; it reported Berlin, Nagoya and Teresina.

## Tavily and UpRock side by side

Tavily numbers come from its pricing and docs pages, checked 24 Oct 2026. Tavily bills in credits: $0.008 each pay-as-you-go, or $0.0075 on the Project plan ($30 for 4,000 credits a month).

| | Tavily | UpRock |
|---|---|---|
| Page extraction, per 1,000 pages | [$1.60](https://docs.tavily.com/documentation/api-credits) pay-as-you-go, [$1.50](https://www.tavily.com/pricing) on Project (5 basic extractions per credit). Failed URLs are free | $0.50 by card or USDC, $0.40 with UPT |
| Search, per 1,000 | [$8.00](https://docs.tavily.com/documentation/api-credits) pay-as-you-go, $7.50 on Project (basic search, 1 credit); advanced search costs double | Not compared here. Use Tavily for search |
| Country | [`country` boosts search results](https://docs.tavily.com/documentation/api-reference/endpoint/search) from one country (general topic only). [Extract](https://docs.tavily.com/documentation/api-reference/endpoint/extract) has no country option | Per page: 63 countries, or a region |
| Where pages are fetched from | Not stated in Tavily's docs | Real desktop and mobile devices whose owners opted in |
| Answers, topic and domain filters | Yes | No. Your model writes the answer |
| Framework integrations | LangChain's recommended search tool, about 40 integrations | MCP, CLI, a Claude Code plugin, an n8n node |
| MCP server | [Official](https://docs.tavily.com/documentation/mcp), remote, with OAuth | Yes, remote (streamable HTTP) |
| Rate limits | [100 requests a minute](https://docs.tavily.com/documentation/rate-limits) on development keys, 1,000 on production keys | Not published yet |
| Video | No | Search public video by moment, $1 per 1,000 searches |
| To try it | 1,000 credits a month at no cost, and a keyless mode | Prepaid credits |

Where Tavily is better: search itself (ranked results ready for a model, generated answers, filters), framework support and a way to try it before paying. Where UpRock fits: the pages Extract couldn't read, a page as it looks in one country, and video.

## Retry Tavily's failed extracts with UpRock

Every version runs Tavily Extract first, then send only the failed URLs to UpRock, all at once. UpRock gets one `POST /v1/crawl` request with your key in an `Authorization: Bearer` header; the MCP and CLI tabs do the same through UpRock's MCP server (TypeScript) or CLI (Python).

### TypeScript (HTTP)

```ts
// npm i @tavily/core   (Node 18+)   env: TAVILY_API_KEY, UPROCK_API_KEY
import { tavily } from "@tavily/core";

const tvly = tavily({ apiKey: process.env.TAVILY_API_KEY });

type Page = { url: string; content: string; via: "tavily" | "uprock" };

async function viaUpRock(url: string, country: string): Promise<Page> {
  // One call: UpRock loads the page on a real device in that country, tries a fresh device
  // every 7 s (up to 3 tries) and returns markdown. Failed calls aren't billed.
  const res = await fetch("https://edge.uprock.com/v1/crawl", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.UPROCK_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ url, country }),
    signal: AbortSignal.timeout(90_000),
  });
  const page = await res.json();
  if (!res.ok) throw new Error(`UpRock ${res.status} ${page.code}: ${page.error}`);
  return { url, content: page.markdown ?? "", via: "uprock" };
}

// Tavily Extract first; its failedResults (free on Tavily's side) go to UpRock in parallel.
// Extract has no country option, so a read that must come from one country skips Tavily.
export async function extract(urls: string[], country?: string) {
  const pages: Page[] = [];
  let failed = urls;
  if (!country) {
    try {
      const res = await tvly.extract(urls, { extractDepth: "basic" });
      pages.push(
        ...res.results.map((r) => ({
          url: r.url,
          content: r.rawContent,
          via: "tavily" as const,
        })),
      );
      failed = res.failedResults.map((f) => f.url);
    } catch (err) {
      console.warn(
        `Tavily Extract failed (${(err as Error).message}); sending every URL to UpRock`,
      );
    }
  }
  const retries = await Promise.allSettled(
    failed.map((url) => viaUpRock(url, country ?? "US")),
  );
  const stillFailed: { url: string; error: string }[] = [];
  retries.forEach((r, i) =>
    r.status === "fulfilled"
      ? pages.push(r.value)
      : stillFailed.push({ url: failed[i], error: String(r.reason) }),
  );
  return { pages, stillFailed };
}

// node extract.ts DE https://developer.mozilla.org/en-US/docs/Web/HTTP https://example.org   (country "-" = any)
const [country, ...urls] = process.argv.slice(2);
const { pages, stillFailed } = await extract(
  urls,
  country === "-" ? undefined : country,
);
for (const p of pages) console.log(p.via.padEnd(6), p.url, p.content.length, "chars");
for (const f of stillFailed) console.log("failed", f.url, f.error);
```

### Python (HTTP)

```python
# pip install tavily-python requests   env: TAVILY_API_KEY, UPROCK_API_KEY
import os
import sys
from concurrent.futures import ThreadPoolExecutor

import requests
from tavily import TavilyClient

tavily = TavilyClient()  # reads TAVILY_API_KEY


def via_uprock(url: str, country: str) -> dict:
    # One call: UpRock loads the page on a real device in that country, tries a fresh device
    # every 7 s (up to 3 tries) and returns markdown. Failed calls aren't billed.
    res = requests.post(
        "https://edge.uprock.com/v1/crawl",
        headers={"Authorization": f"Bearer {os.environ['UPROCK_API_KEY']}"},
        json={"url": url, "country": country},
        timeout=90,
    )
    page = res.json()
    if not res.ok:
        raise RuntimeError(f"UpRock {res.status_code} {page.get('code')}: {page.get('error')}")
    return {
        "url": url,
        "content": page.get("markdown", ""),
        "via": "uprock",
    }


def extract(
    urls: list[str], country: str | None = None
) -> tuple[list[dict], list[dict]]:
    """Tavily Extract first; failed_results (free on Tavily's side) go to UpRock in parallel.
    Extract has no country option, so a read that must come from one country skips Tavily."""
    pages, failed = [], list(urls)
    if country is None:
        try:
            res = tavily.extract(urls=urls, extract_depth="basic")
            pages = [
                {"url": r["url"], "content": r["raw_content"], "via": "tavily"}
                for r in res["results"]
            ]
            failed = [f["url"] for f in res.get("failed_results", [])]
        except Exception as err:
            print(
                f"Tavily Extract failed ({err}); sending every URL to UpRock",
                file=sys.stderr,
            )

    still_failed = []
    with ThreadPoolExecutor(max_workers=8) as pool:
        futures = {url: pool.submit(via_uprock, url, country or "US") for url in failed}
    for url, future in futures.items():
        try:
            pages.append(future.result())
        except Exception as err:
            still_failed.append({"url": url, "error": str(err)})
    return pages, still_failed


if __name__ == "__main__":
    # python extract.py DE https://developer.mozilla.org/en-US/docs/Web/HTTP https://example.org   (country "-" = any)
    country, *urls = sys.argv[1:]
    pages, still_failed = extract(urls, None if country == "-" else country)
    for p in pages:
        print(p["via"].ljust(6), p["url"], len(p["content"]), "chars")
    for f in still_failed:
        print("failed", f["url"], f["error"])
```

### TypeScript (MCP)

```ts
// npm i @tavily/core @modelcontextprotocol/sdk   (Node 18+)   env: TAVILY_API_KEY, UPROCK_API_KEY
import { tavily } from "@tavily/core";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const tvly = tavily({ apiKey: process.env.TAVILY_API_KEY });
const uprock = new Client({ name: "tavily-fallback", version: "1.0.0" });
await uprock.connect(
  new StreamableHTTPClientTransport(
    new URL("https://mcp.uprock.ai/mcp"),
    { requestInit: { headers: { Authorization: `Bearer ${process.env.UPROCK_API_KEY}` } } },
  ),
);

type Page = { url: string; content: string; via: "tavily" | "uprock" };

const textOf = (content: unknown) =>
  (content as { type: string; text?: string }[]).find((c) => c.type === "text")?.text ??
  "";

async function viaUpRock(url: string, country: string): Promise<Page> {
  const res = await uprock.callTool(
    { name: "crawl_fetch", arguments: { url, country } },
    undefined,
    { timeout: 300_000 },
  );
  if (res.isError) throw new Error(textOf(res.content));
  const uri = JSON.parse(textOf(res.content)).content?.markdown?.resource;
  if (!uri) throw new Error("no markdown for this page");
  const md = await uprock.callTool({ name: "resource_fetch", arguments: { uri } });
  return { url, content: textOf(md.content), via: "uprock" };
}

// Tavily Extract first; its failedResults (free on Tavily's side) go to UpRock in parallel.
// Extract has no country option, so a read that must come from one country skips Tavily.
export async function extract(urls: string[], country?: string) {
  const pages: Page[] = [];
  let failed = urls;
  if (!country) {
    try {
      const res = await tvly.extract(urls, { extractDepth: "basic" });
      pages.push(
        ...res.results.map((r) => ({
          url: r.url,
          content: r.rawContent,
          via: "tavily" as const,
        })),
      );
      failed = res.failedResults.map((f) => f.url);
    } catch (err) {
      console.warn(
        `Tavily Extract failed (${(err as Error).message}); sending every URL to UpRock`,
      );
    }
  }
  const retries = await Promise.allSettled(
    failed.map((url) => viaUpRock(url, country ?? "US")),
  );
  const stillFailed: { url: string; error: string }[] = [];
  retries.forEach((r, i) =>
    r.status === "fulfilled"
      ? pages.push(r.value)
      : stillFailed.push({ url: failed[i], error: String(r.reason) }),
  );
  return { pages, stillFailed };
}

// node extract.ts DE https://developer.mozilla.org/en-US/docs/Web/HTTP https://example.org   (country "-" = any)
const [country, ...urls] = process.argv.slice(2);
const { pages, stillFailed } = await extract(
  urls,
  country === "-" ? undefined : country,
);
for (const p of pages) console.log(p.via.padEnd(6), p.url, p.content.length, "chars");
for (const f of stillFailed) console.log("failed", f.url, f.error);
await uprock.close();
```

### Python (CLI)

```python
# pip install tavily-python; npm i -g uprock   env: TAVILY_API_KEY, UPROCK_API_KEY
import json
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor

from tavily import TavilyClient

tavily = TavilyClient()  # reads TAVILY_API_KEY


def via_uprock(url: str, country: str) -> dict:
    run = subprocess.run(
        ["uprock", "ai", "crawl", url, "--country", country, "--content"],
        capture_output=True,
        text=True,
        timeout=330,
    )
    if run.returncode != 0:
        raise RuntimeError(run.stderr.strip())
    return {
        "url": url,
        "content": json.loads(run.stdout).get("inlined_markdown", ""),
        "via": "uprock",
    }


def extract(
    urls: list[str], country: str | None = None
) -> tuple[list[dict], list[dict]]:
    """Tavily Extract first; failed_results (free on Tavily's side) go to UpRock in parallel.
    Extract has no country option, so a read that must come from one country skips Tavily."""
    pages, failed = [], list(urls)
    if country is None:
        try:
            res = tavily.extract(urls=urls, extract_depth="basic")
            pages = [
                {"url": r["url"], "content": r["raw_content"], "via": "tavily"}
                for r in res["results"]
            ]
            failed = [f["url"] for f in res.get("failed_results", [])]
        except Exception as err:
            print(
                f"Tavily Extract failed ({err}); sending every URL to UpRock",
                file=sys.stderr,
            )

    still_failed = []
    with ThreadPoolExecutor(max_workers=8) as pool:
        futures = {url: pool.submit(via_uprock, url, country or "US") for url in failed}
    for url, future in futures.items():
        try:
            pages.append(future.result())
        except Exception as err:
            still_failed.append({"url": url, "error": str(err)})
    return pages, still_failed


if __name__ == "__main__":
    # python extract.py DE https://developer.mozilla.org/en-US/docs/Web/HTTP https://example.org   (country "-" = any)
    country, *urls = sys.argv[1:]
    pages, still_failed = extract(urls, None if country == "-" else country)
    for p in pages:
        print(p["via"].ljust(6), p["url"], len(p["content"]), "chars")
    for f in still_failed:
        print("failed", f["url"], f["error"])
```

Tested 19 Oct 2026 against the live UpRock API: the TypeScript version with Tavily's keyless mode (Tavily read 2 of 4 URLs, and UpRock fetched the failed ones that exist), the Python version with an invalid Tavily key. On 6 Nov 2026 we ran the HTTP and MCP versions again with an UpRock API key in an `Authorization` header: with an invalid Tavily key, each fell back to UpRock and got the page.

## FAQ

### Does Tavily charge for an extract that fails?

No. Tavily's credits page says you never get charged if a URL extraction fails. Basic extraction costs 1 credit per 5 successful URLs, so a fallback only adds the UpRock page.

### What does Tavily's country parameter do?

It boosts search results from the country you name, for general-topic searches. It doesn't change where Extract reads a page from, and Extract has no country option of its own, so a page that differs by country needs a fetch from that country.

### Can UpRock replace Tavily?

For most agents, no. Tavily ranks results for LLMs, can write an answer for you, filters by topic, date and domain, and plugs into LangChain and dozens of other frameworks. UpRock is the second tool for pages and places Tavily doesn't cover.

### Why does the TypeScript example use MCP instead of plain HTTP?

UpRock's MCP tool crawl_fetch retries on a fresh device every 7 seconds until one succeeds and returns markdown. The plain HTTP crawl API takes three calls and returns the page as HTML, so MCP (or the CLI, as in the Python example) is the shorter path today.

### What does UpRock cost next to Tavily?

An UpRock page fetch costs $0.50 per 1,000 by card or USDC, $0.40 with UPT. Tavily's basic extraction works out to $1.60 per 1,000 pages pay-as-you-go and $1.50 on its $30 Project plan.

## Other use cases to explore

- [Fetch from countries your crawler doesn't cover](https://uprock.ai/compare/firecrawl#when-a-second-crawler-helps): such as South Korea and Indonesia, sent straight to a device in that country.
- [Decide which failed pages are worth retrying](https://uprock.ai/use-cases/agent-gets-403): a diagnosis table from status code to the legitimate fix, and when to stop.
- [Estimate what 1,000 pages will cost](https://uprock.ai/compare/jina-reader#tokens-or-pages-what-1000-reads-cost): token counts for six real pages, and the length where a flat per-page price is cheaper.
- [Search public video by moment](https://uprock.ai/video-semantic-search): find the second something is shown or said, across platforms.

## Related articles

- [How do I add a backup crawler to Firecrawl?](https://uprock.ai/compare/firecrawl.md): When to send a page from Firecrawl to UpRock, published prices side by side, and a tested fallback in TypeScript and Python.
- [How do I add a fallback for Exa contents errors?](https://uprock.ai/compare/exa.md): Read the per-URL statuses in Exa's /contents response and retry the right ones live on a real device. Error tags explained, prices side by side, tested code.
- [How do I add a backup to Jina Reader, and when is it cheaper?](https://uprock.ai/compare/jina-reader.md): Spot a failed read in Reader's JSON (it still answers 200), retry it on a real device, and see what six real pages cost in Reader tokens next to a flat per-page price.
- [How do I add a hosted fallback to Crawl4AI?](https://uprock.ai/compare/crawl4ai.md): Plug UpRock into Crawl4AI's fallback_fetch_function so blocked pages are fetched on a real device in the country you choose, then run through Crawl4AI's own markdown pipeline.
- [How do I add a backup to Bright Data's Web Unlocker?](https://uprock.ai/compare/bright-data.md): Which Web Unlocker errors to retry, which to send to a second network and which to respect, with Bright Data's published prices next to ours and tested fallback code.
- [How do I add a fallback to an Apify or Crawlee crawler?](https://uprock.ai/compare/apify.md): Hand the requests Crawlee gives up on to a second network with failedRequestHandler, see what a fetched page costs on Apify's own Actors, and run the tested code.
- [When should a Browserbase agent read a page without opening a browser?](https://uprock.ai/compare/browserbase.md): What 1,000 page reads cost on Browserbase three ways, when to send a read to UpRock instead (including when every browser is busy), and tested fallback code.
- [How do I cap ScrapingBee's credit cost and send the rest to a backup?](https://uprock.ai/compare/scrapingbee.md): ScrapingBee charges 1 to 75 credits a page depending on the options. Cap it with Auto-Mode and max_cost, watch the country fallback, and send the rest to UpRock with tested code.
- [Spider costs less per page than UpRock. When is a backup still worth it?](https://uprock.ai/compare/spider.md): Spider is cheaper per page, so keep it as your primary. How Spider reports a page it couldn't get (from our own tests), and tested code that sends only those pages to UpRock.
- [How do I add a fallback to Parallel's Extract API?](https://uprock.ai/compare/parallel.md): Make Parallel Extract report a failed live fetch instead of quietly serving an old cached copy, then retry those URLs on UpRock. Prices side by side and tested code.
- [How do I add a backup to ZenRows?](https://uprock.ai/compare/zenrows.md): Which ZenRows error codes to send to a second network and which to respect, what a page from one country costs once premium proxies are required, and tested fallback code.
- [How do I retry a ScraperAPI batch's failed jobs on another network?](https://uprock.ai/compare/scraperapi.md): Send the jobs ScraperAPI's async batch gives up on to UpRock, see which countries each ScraperAPI plan can target, and compare credit costs. Tested code for both.
- [How do I add a backup to Oxylabs' new Web API?](https://uprock.ai/compare/oxylabs.md): Which Oxylabs Web API failures to send to a second network and which to respect, what a generic page really costs after its October 2026 pricing change, and tested fallback code.
- [How do I add a backup to Context.dev's scrape API?](https://uprock.ai/compare/context-dev.md): Context.dev reports a failed scrape inside an HTTP 200. Which of its error codes to retry on a second network, what a page costs by plan, and tested fallback code.
- [How do I add a backup to Scrapfly instead of paying for residential?](https://uprock.ai/compare/scrapfly.md): A Scrapfly page costs 1 to 30 credits depending on proxy pool and browser. What that is per 1,000 pages on each plan, which errors to send on, and tested code.
- [How do I keep Crawlbase's site tiers from surprising my bill?](https://uprock.ai/compare/crawlbase.md): Crawlbase bills each site at 1 to 20 times its base rate. What that does to the cost per 1,000 pages, and tested code that moves pricey domains to UpRock.
- [Which unlocker API fits: Bright Data, Oxylabs, Zyte or UpRock?](https://uprock.ai/compare/web-unlocker-apis.md): Bright Data's Web Unlocker, Oxylabs' Web API and Zyte API priced per 1,000 hard pages, with what each bills and when a second network helps.
- [Compare: all articles](https://uprock.ai/compare.md)

## References

1. [Tavily, Pricing](https://www.tavily.com/pricing), checked 24 Oct 2026
2. [Tavily docs, API credits (plans and credit cost per endpoint)](https://docs.tavily.com/documentation/api-credits), checked 24 Oct 2026
3. [Tavily docs, Search endpoint (the country parameter)](https://docs.tavily.com/documentation/api-reference/endpoint/search), checked 19 Oct 2026
4. [Tavily docs, Extract endpoint (failed_results, limits)](https://docs.tavily.com/documentation/api-reference/endpoint/extract), checked 19 Oct 2026
5. [Tavily docs, Rate limits](https://docs.tavily.com/documentation/rate-limits), checked 24 Oct 2026
6. [Tavily docs, MCP server](https://docs.tavily.com/documentation/mcp), checked 19 Oct 2026
7. [Tavily JavaScript SDK reference](https://docs.tavily.com/sdk/javascript/reference), checked 19 Oct 2026
8. [UpRock, everything in one file (API, CLI, MCP, pricing)](https://uprock.ai/llms-full.txt), checked 24 Oct 2026

## Get started

Get an API key: https://uprock.ai/app/api-keys

Paste this into your agent:

```text
Set up UpRock for me: read https://uprock.ai/agents.md, connect the UpRock MCP server, and run one test crawl. Tell me what it costs before you spend anything.
```

Setup for every client (MCP, skill, CLI): https://uprock.ai/agents.md
