How do I add a fallback to Parallel's Extract API?
Keep Parallel for search and extraction, and send UpRock the URLs Extract couldn't fetch, along with any page you need from a specific country, which Extract can't do. Turn off its cache fallback first, because by default a failed live fetch quietly returns an older cached copy instead of an error.
Updated · Read as markdown
What Parallel does well
Parallel builds web search and extraction for AI agents on top of its own index, which it describes as hundreds of billions of pages, including licensed content from paywalled publishers. Its Search API returns results with excerpts chosen for your objective, and Parallel publishes benchmarks where it leads (its own September 2026 runs put Parallel Advanced at 97% on a 100-question SimpleQA sample). Extract turns any public URL into markdown, JavaScript-heavy pages and PDFs included, the Task API runs multi-step research jobs, FindAll turns a description into a verified list of matching entities, and Monitor reruns a query on a schedule and notifies you when the answer changes. There are Python and TypeScript SDKs, a CLI, integrations with LangChain, Vercel, n8n and Zapier, and a Search MCP server anyone can use without a key.
For finding and summarizing information, Parallel is strong. Keep it.
The cache-fallback trap
Extract can answer from Parallel's cache (fast) or fetch the page live (slower). fetch_policy controls that: max_age_seconds sets how old a cached copy may be (at least 10 minutes), timeout_seconds how long a live fetch may take.
The setting that matters for a backup is disable_cache_fallback. Its default is false, and Parallel's docs describe what that means: if a live fetch fails, Extract "falls back to older indexed content". So a page that has changed, or that refused Parallel's fetcher today, can come back looking fine, with yesterday's content.
Set it to true, and a failed live fetch shows up where you can act on it, in the response's errors list:
| What Extract returns | What it means | What to do |
|---|---|---|
A result with full_content | Parallel got the page (live or from cache) | Use it |
An errors entry, http_status_code 401, 403, 429 or 5xx | The site refused or failed the live fetch | Send to UpRock |
An errors entry, error_type like fetch_error, no status | The fetch failed for another reason | Send to UpRock |
An errors entry, http_status_code 404 or 410 | The page is gone | Don't retry |
| HTTP 401, 402, 422, 429 or 5xx for the whole request | A bad key, no credit, a bad body, the rate limit or a Parallel error | Send every URL to UpRock (and fix the request if it's a 422) |
| You need the page from one country | Extract has no country option | Send to UpRock |
The HTTP response is 200 even when some URLs failed. error_type is a free-form string; Parallel doesn't publish a list of values.
Parallel and UpRock side by side
Parallel's numbers come from its pricing pages and docs, checked 24 Oct 2026.
| Parallel | UpRock | |
|---|---|---|
| Page extraction, per 1,000 | $1 per 1,000 URLs, excerpts or full content | $0.50 by card or USDC, $0.40 with UPT |
| Search, per 1,000 | $1 (fast, turbo) or $5 (basic, advanced, the default) for 10 results; $1 per 1,000 extra results | Web research, $1 ($0.80 with UPT), across several search engines. Most results were off-topic in our 24 Oct tests, so use Parallel for search for now |
| Research tasks, entity lists, monitoring | Task $5 to $2,400 per 1,000 runs, by processor; FindAll $0.10 to $10 a run plus up to $1 a match; Monitor $3 or $10 per 1,000 runs | None yet |
| Own index | Yes, including licensed content | No. Every call fetches the page |
| Latency | 1 to 3 s cached, 60 to 90 s live (Extract) | 13 to 37 s fresh in our tests, about 1 s for a recent repeat |
| Country | Search: location, 37 countries. Extract: none | Every request: 63 countries, or a region |
| Where pages are fetched from | Not stated; user-requested fetches identify as Shap-User | Devices whose owners opted in |
| Rate limits | 600 requests a minute for Extract and Search | Not published yet |
| MCP server | Search MCP, free without a key; Task MCP with a key | Yes, remote (streamable HTTP) |
| Video | No | Search public video by moment, $1 per 1,000 searches |
| To try it | Up to 5,000 requests a month at no cost on pay-as-you-go, up to $80 at signup and $5 a month | Prepaid credits |
Where Parallel is better: search quality and its own index, excerpts tuned to your objective, fast cached reads, a free MCP server, credits to try it, and three products UpRock has no equivalent for yet: Task, FindAll and Monitor. Where UpRock fits: the live fetches that failed, a page from a chosen country, a lower price per page, and video.
The fallback, in TypeScript and Python
Every version call Extract with full_content and disable_cache_fallback: true, keep the pages that came back, skip the ones the site says are gone, and fetch the rest on UpRock in parallel. The SDK's timeout is raised past a minute and its retries are switched off, so a slow live fetch doesn't stall the batch. 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).
// npm i parallel-web (Node 18+) env: PARALLEL_API_KEY, UPROCK_API_KEYimport Parallel from "parallel-web";// A live fetch can take a minute; give the SDK room past its 60 s default and skip its retries.const parallel = new Parallel({ apiKey: process.env.PARALLEL_API_KEY, timeout: 120_000, maxRetries: 0,});async function viaUpRock(url: string, country: string) { // 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 page.markdown ?? "";}export async function extract(urls: string[], country = "US") { const pages = new Map<string, { via: string; markdown: string }>(); const gone = new Set<string>(); try { const res = await parallel.extract({ urls, advanced_settings: { full_content: true, // By default, a failed live fetch quietly falls back to an older cached copy. Turn that // off so failures show up in `errors`, where the code below can send them to UpRock. fetch_policy: { disable_cache_fallback: true, timeout_seconds: 60 }, }, }); for (const r of res.results) { if (r.full_content) pages.set(r.url, { via: "parallel", markdown: r.full_content }); } for (const e of res.errors) { // The site says the page is gone: a second crawler will see the same. if (e.http_status_code === 404 || e.http_status_code === 410) gone.add(e.url); console.warn(`Parallel: ${e.url} ${e.error_type} ${e.http_status_code ?? ""}`); } } catch (err) { // 401, 402 (out of credit), 422, 429 or 5xx: the whole request failed. console.warn( `Parallel failed (${(err as Error).message}); sending every URL to UpRock`, ); } // Extract has no country option, so the retry is also how you get a page from one country. const retry = urls.filter((u) => !pages.has(u) && !gone.has(u)); const retried = await Promise.allSettled(retry.map((u) => viaUpRock(u, country))); retried.forEach((r, i) => { if (r.status === "fulfilled") pages.set(retry[i], { via: "uprock", markdown: r.value }); else console.warn(`UpRock also failed for ${retry[i]}: ${r.reason}`); }); return pages;}// node extract.ts DE https://developer.mozilla.org/en-US/docs/Web/HTTP https://example.orgconst [country = "US", ...urls] = process.argv.slice(2);for (const [url, page] of await extract(urls, country)) { console.log(page.via.padEnd(8), url, page.markdown.length, "chars");}# pip install parallel-web requests env: PARALLEL_API_KEY, UPROCK_API_KEYimport osimport sysfrom concurrent.futures import ThreadPoolExecutorimport requestsfrom parallel import Parallel# A live fetch can take a minute; give the SDK room past its 60 s default and skip its retries.parallel = Parallel( api_key=os.environ.get("PARALLEL_API_KEY"), timeout=120, max_retries=0)def via_uprock(url: str, country: str) -> str: # 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 page.get("markdown", "")def extract(urls: list[str], country: str = "US") -> dict[str, dict]: pages, gone = {}, set() try: res = parallel.extract( urls=urls, advanced_settings={ "full_content": True, # By default, a failed live fetch quietly falls back to an older cached copy. # Turn that off so failures show up in `errors` and can go to UpRock. "fetch_policy": {"disable_cache_fallback": True, "timeout_seconds": 60}, }, ) for r in res.results: if r.full_content: pages[r.url] = {"via": "parallel", "markdown": r.full_content} for e in res.errors: if e.http_status_code in (404, 410): gone.add(e.url) # the site says the page is gone print( f"Parallel: {e.url} {e.error_type} {e.http_status_code or ''}", file=sys.stderr, ) except Exception as err: # 401, 402 (out of credit), 422, 429 or 5xx: the whole request failed. print(f"Parallel failed ({err}); sending every URL to UpRock", file=sys.stderr) # Extract has no country option, so the retry is also how you get a page from one country. retry = [u for u in urls if u not in pages and u not in gone] with ThreadPoolExecutor(max_workers=8) as pool: futures = {u: pool.submit(via_uprock, u, country) for u in retry} for url, future in futures.items(): try: pages[url] = {"via": "uprock", "markdown": future.result()} except Exception as err: print(f"UpRock also failed for {url}: {err}", file=sys.stderr) return pagesif __name__ == "__main__": # python extract.py DE https://developer.mozilla.org/en-US/docs/Web/HTTP https://example.org country, *urls = sys.argv[1:] or ["US"] for url, page in extract(urls, country).items(): print(page["via"].ljust(8), url, len(page["markdown"]), "chars")// npm i parallel-web @modelcontextprotocol/sdk (Node 18+) env: PARALLEL_API_KEY, UPROCK_API_KEYimport Parallel from "parallel-web";import { Client } from "@modelcontextprotocol/sdk/client/index.js";import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";// A live fetch can take a minute; give the SDK room past its 60 s default and skip its retries.const parallel = new Parallel({ apiKey: process.env.PARALLEL_API_KEY, timeout: 120_000, maxRetries: 0,});const uprock = new Client({ name: "parallel-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}` } } }, ),);const textOf = (content: unknown) => (content as { type: string; text?: string }[]).find((c) => c.type === "text")?.text ?? "";async function viaUpRock(url: string, country: string) { const res = await uprock.callTool( { name: "crawl_fetch", arguments: { url, country } }, undefined, { timeout: 300_000 }, ); if (res.isError) throw new Error(`UpRock: ${textOf(res.content)}`); const uri = JSON.parse(textOf(res.content)).content?.markdown?.resource; if (!uri) throw new Error(`UpRock: no markdown for ${url}`); return textOf( (await uprock.callTool({ name: "resource_fetch", arguments: { uri } })).content, );}export async function extract(urls: string[], country = "US") { const pages = new Map<string, { via: string; markdown: string }>(); const gone = new Set<string>(); try { const res = await parallel.extract({ urls, advanced_settings: { full_content: true, // By default, a failed live fetch quietly falls back to an older cached copy. Turn that // off so failures show up in `errors`, where the code below can send them to UpRock. fetch_policy: { disable_cache_fallback: true, timeout_seconds: 60 }, }, }); for (const r of res.results) { if (r.full_content) pages.set(r.url, { via: "parallel", markdown: r.full_content }); } for (const e of res.errors) { // The site says the page is gone: a second crawler will see the same. if (e.http_status_code === 404 || e.http_status_code === 410) gone.add(e.url); console.warn(`Parallel: ${e.url} ${e.error_type} ${e.http_status_code ?? ""}`); } } catch (err) { // 401, 402 (out of credit), 422, 429 or 5xx: the whole request failed. console.warn( `Parallel failed (${(err as Error).message}); sending every URL to UpRock`, ); } // Extract has no country option, so the retry is also how you get a page from one country. const retry = urls.filter((u) => !pages.has(u) && !gone.has(u)); const retried = await Promise.allSettled(retry.map((u) => viaUpRock(u, country))); retried.forEach((r, i) => { if (r.status === "fulfilled") pages.set(retry[i], { via: "uprock", markdown: r.value }); else console.warn(`UpRock also failed for ${retry[i]}: ${r.reason}`); }); return pages;}// node extract.ts DE https://developer.mozilla.org/en-US/docs/Web/HTTP https://example.orgconst [country = "US", ...urls] = process.argv.slice(2);try { for (const [url, page] of await extract(urls, country)) { console.log(page.via.padEnd(8), url, page.markdown.length, "chars"); }} finally { await uprock.close();}# pip install parallel-web; npm i -g uprock env: PARALLEL_API_KEY, UPROCK_API_KEYimport jsonimport osimport subprocessimport sysfrom concurrent.futures import ThreadPoolExecutorfrom parallel import Parallel# A live fetch can take a minute; give the SDK room past its 60 s default and skip its retries.parallel = Parallel( api_key=os.environ.get("PARALLEL_API_KEY"), timeout=120, max_retries=0)def via_uprock(url: str, country: str) -> str: run = subprocess.run( ["uprock", "ai", "crawl", url, "--country", country, "--content"], capture_output=True, text=True, timeout=330, ) if run.returncode != 0: raise RuntimeError(f"UpRock: {run.stderr.strip()}") return json.loads(run.stdout).get("inlined_markdown", "")def extract(urls: list[str], country: str = "US") -> dict[str, dict]: pages, gone = {}, set() try: res = parallel.extract( urls=urls, advanced_settings={ "full_content": True, # By default, a failed live fetch quietly falls back to an older cached copy. # Turn that off so failures show up in `errors` and can go to UpRock. "fetch_policy": {"disable_cache_fallback": True, "timeout_seconds": 60}, }, ) for r in res.results: if r.full_content: pages[r.url] = {"via": "parallel", "markdown": r.full_content} for e in res.errors: if e.http_status_code in (404, 410): gone.add(e.url) # the site says the page is gone print( f"Parallel: {e.url} {e.error_type} {e.http_status_code or ''}", file=sys.stderr, ) except Exception as err: # 401, 402 (out of credit), 422, 429 or 5xx: the whole request failed. print(f"Parallel failed ({err}); sending every URL to UpRock", file=sys.stderr) # Extract has no country option, so the retry is also how you get a page from one country. retry = [u for u in urls if u not in pages and u not in gone] with ThreadPoolExecutor(max_workers=8) as pool: futures = {u: pool.submit(via_uprock, u, country) for u in retry} for url, future in futures.items(): try: pages[url] = {"via": "uprock", "markdown": future.result()} except Exception as err: print(f"UpRock also failed for {url}: {err}", file=sys.stderr) return pagesif __name__ == "__main__": # python extract.py DE https://developer.mozilla.org/en-US/docs/Web/HTTP https://example.org country, *urls = sys.argv[1:] or ["US"] for url, page in extract(urls, country).items(): print(page["via"].ljust(8), url, len(page["markdown"]), "chars")Tested 21 Oct 2026 against the live UpRock API with an invalid Parallel key (a 401, so every URL goes to UpRock). Without a Parallel account, the per-URL errors path follows Parallel's docs and SDK types (parallel-web 1.3.6). On 6 Nov 2026 we ran the HTTP and MCP versions again with an UpRock API key in an Authorization header: with an invalid Parallel key, each fell back to UpRock and got the page.
FAQ
What does disable_cache_fallback do?
It decides what Parallel does when a live fetch fails or times out. With the default (false), Extract falls back to older indexed content and returns that as if it were fine. With true, it returns an error for that URL instead, which is what a fallback needs to see.
Can Parallel Extract fetch a page from a specific country?
Not according to its docs. Parallel's Search takes a location (37 countries are listed), but Extract's settings cover only the fetch policy, excerpts and full content. Parallel's docs don't say where its fetches come from. UpRock takes a country on every request.
Is UpRock cheaper than Parallel?
For fetching pages, yes. Extract costs $1 per 1,000 URLs; UpRock costs $0.50 per 1,000 pages by card or USDC, though Parallel's price also covers excerpts chosen for your objective. For search, Parallel's turbo and fast modes cost $1 per 1,000, the same as UpRock's web research at $1. Most of the results UpRock's research returned in our 24 Oct tests were off-topic, so use Parallel for search for now.
How fast is each?
Parallel's pricing page puts a cached Extract at 1 to 3 seconds and a live one at 60 to 90 seconds. In our tests a fresh UpRock fetch took 13 to 37 seconds end to end, and a recently fetched page came back in about a second.
Does Parallel have a free MCP server?
Yes. Its Search MCP at search.parallel.ai/mcp works anonymously, with an optional API key for higher limits, and includes a web_fetch tool for up to 20 URLs.
Other use cases to explore
- Decide which failed pages are worth retrying: a diagnosis table from status code to the legitimate fix, and when to stop.
- See a page as it looks in another country: one store page from five countries, with five prices, two languages and a cookie banner in one country only.
- Work out what a browser read really costs: per-minute billing, proxy megabytes and a fetch API, side by side for 1,000 pages.
- Search public video by moment: find the second something is shown or said, across platforms.
References
- Parallel docs, Pricing, checked 24 Oct 2026
- Parallel, Extract product page, checked 21 Oct 2026
- Parallel, Pricing (free credits, latency), checked 24 Oct 2026
- Parallel docs, Advanced extract settings (fetch policy), checked 21 Oct 2026
- Parallel docs, Advanced search settings (location), checked 21 Oct 2026
- Parallel docs, Rate limits, checked 24 Oct 2026
- Parallel docs, Search MCP, checked 21 Oct 2026
- Parallel, Bots and crawlers, checked 21 Oct 2026
- UpRock, everything in one file (API, CLI, MCP, pricing), checked 24 Oct 2026
Get started
Add UpRock in one paste.
Paste this into Claude Code, Codex, Cursor or any agent with a terminal. It reads our docs, sets up the MCP server or CLI, and tells you the cost before it spends anything.
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.