Production Reliability
Caching behavior, retry handling, error patterns, timeouts, and operational decisions for running Tabstack dependably in production.
This guide covers what you need to know to run Tabstack dependably in production: caching behavior, when to bypass it, retry handling, error patterns, and the operational decisions that matter at scale.
Caching
Section titled “Caching”Tabstack caches extraction results by URL. When you request the same URL again, the API may return a cached result rather than re-fetching the page. This is usually what you want: it’s faster and reduces cost.
What’s cached: The response for a given URL + endpoint combination. Effort level and schema are also factors; changing them may produce a fresh fetch.
How long results are cached: Cache TTL is not publicly documented. For time-sensitive data (pricing, inventory, live stats), always set nocache: true.
When to use nocache: true:
- Pricing or availability data that changes frequently
- Pages you know have updated since your last fetch
- Debugging extraction issues (confirms the problem isn’t a stale cache)
- Any data with a business requirement for freshness
// Price monitoring: always freshconst result = await client.extract.json({ url: "https://competitor.com/pricing", nocache: true, json_schema: { /* ... */ },});# Price monitoring, always freshresult = client.extract.json( url="https://competitor.com/pricing", nocache=True, json_schema={...},)curl -X POST https://api.tabstack.ai/v1/extract/json \ -H "Authorization: Bearer $TABSTACK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://competitor.com/pricing", "nocache": true, "json_schema": { "type": "object" } }'Automatic retries
Section titled “Automatic retries”The SDK automatically retries failed requests twice with exponential backoff. The following errors are retried:
| Status | Error |
|---|---|
| 408 | Request Timeout |
| 409 | Conflict |
| 429 | Rate Limit |
| 500+ | Server errors |
| N/A | Network / connection failures |
Retries happen transparently. You don’t need to implement retry logic for these cases.
Disable retries if you’re running your own retry orchestration:
const client = new Tabstack({ apiKey: process.env.TABSTACK_API_KEY, maxRetries: 0,});
// Or per-request:await client.extract.json({ url, json_schema }, { maxRetries: 5 });client = Tabstack(max_retries=0)
# Or per-request:client.with_options(max_retries=5).extract.json(url=url, json_schema=json_schema)Error handling patterns
Section titled “Error handling patterns”Distinguish 422 from 500
Section titled “Distinguish 422 from 500”A 422 UnprocessableEntityError means the URL was malformed or inaccessible, or the input was invalid (for example a bad schema). It does not mean the page was fetched but extraction failed. That surfaces as a 500. See the Error Reference for the full definition.
Retrying with effort: 'max' only helps a 500 where the page was fetched but extraction failed because it needed full browser rendering. It will not fix a 422: a genuine malformed URL, DNS, connection, SSL, 404, or robots failure needs a corrected or different URL, not a retry.
import Tabstack, { InternalServerError } from "@tabstack/sdk";
try { return await client.extract.json({ url, json_schema });} catch (err) { if (err instanceof InternalServerError) { // Page was fetched but extraction failed: retry with full rendering return await client.extract.json({ url, json_schema, effort: "max", nocache: true, }); } // A 422 (UnprocessableEntityError) means the URL is bad: fix it, do not retry. throw err;}from tabstack import InternalServerError
try: return client.extract.json(url=url, json_schema=json_schema)except InternalServerError: # Page was fetched but extraction failed: retry with full rendering return client.extract.json( url=url, json_schema=json_schema, effort="max", nocache=True, )# A 422 (UnprocessableEntityError) means the URL is bad: fix it, do not retry.Rate limit handling
Section titled “Rate limit handling”429 errors are auto-retried, but if you’re hitting them consistently you’re exceeding your plan’s rate limit. Use exponential backoff in your own queuing layer for bulk operations:
import { RateLimitError } from "@tabstack/sdk";
async function extractWithBackoff( url: string, schema: object, attempt = 0,): Promise<unknown> { try { return await client.extract.json({ url, json_schema: schema }); } catch (err) { if (err instanceof RateLimitError && attempt < 3) { const delay = Math.pow(2, attempt) * 1000; await new Promise((r) => setTimeout(r, delay)); return extractWithBackoff(url, schema, attempt + 1); } throw err; }}import timefrom tabstack import RateLimitError
def extract_with_backoff(url, schema, attempt=0): try: return client.extract.json(url=url, json_schema=schema) except RateLimitError: if attempt < 3: delay = 2 ** attempt time.sleep(delay) return extract_with_backoff(url, schema, attempt + 1) raiseAuth errors in production
Section titled “Auth errors in production”401 AuthenticationError in production usually means a key was rotated or the environment variable wasn’t set in the deployment. These are not retried automatically; they require intervention.
import { AuthenticationError } from "@tabstack/sdk";
if (err instanceof AuthenticationError) { // Alert: this is a configuration problem, not a transient failure alertOps("Tabstack API key invalid or missing");}from tabstack import AuthenticationError
if isinstance(err, AuthenticationError): # Alert: this is a configuration problem, not a transient failure alert_ops("Tabstack API key invalid or missing")Timeouts
Section titled “Timeouts”Default timeout is 60 seconds per request. effort: 'max' on complex SPAs can approach this. Tune per endpoint:
// Longer timeout for heavy pagesconst result = await client.extract.json( { url, json_schema, effort: "max" }, { timeout: 90_000 }, // 90 seconds);# Longer timeout for heavy pages (seconds)result = client.with_options(timeout=90.0).extract.json( url=url, json_schema=json_schema, effort="max")For /automate and /research (streaming), the SDK applies a default timeout of 600,000 ms (10 minutes) when you haven’t set a client timeout, so long-running agents run to completion without you raising it. An explicit shorter client timeout is still honored, so don’t set one below your expected task duration.
Serverless and edge functions
Section titled “Serverless and edge functions”Serverless platforms cap how long a function can run, and that cap is often shorter than a Tabstack call. A synchronous extract or generate call can take up to 60 seconds on effort: 'max', and /automate or /research can stream for several minutes. If your function’s maximum duration is lower than the call, the platform stops the function before Tabstack responds.
Set the function’s max duration above your expected call time:
- Vercel: raise
maxDurationin the route segment config (orvercel.json). The default on Hobby is short, so giveextractandgenerateheadroom formaxeffort, and give/automateand/researchroom for the full task. - AWS Lambda: raise the function timeout to cover the call. Note that API Gateway applies its own integration timeout (29 seconds by default) in front of Lambda, so for anything that can run longer, invoke Lambda asynchronously or run the call off the request path.
For the streaming endpoints, the function also has to stay alive for the whole stream. Run /automate and /research from a route that supports streaming or long-lived responses, not a short synchronous handler. See Streaming patterns for edge-runtime examples.
Logging
Section titled “Logging”Enable debug logging during development to see full request/response detail:
const client = new Tabstack({ apiKey: process.env.TABSTACK_API_KEY, logLevel: "debug", // 'debug' | 'info' | 'warn' | 'error' | 'off'});import os
# Python logging is controlled by an environment variable:os.environ["TABSTACK_LOG"] = "debug" # "info" | "debug"
client = Tabstack()In production, warn (default) is appropriate: it surfaces retry events and errors without noise.
For Python, set TABSTACK_LOG=info in the environment.
Bulk extraction patterns
Section titled “Bulk extraction patterns”For high-volume extraction jobs (many URLs), control concurrency to avoid rate limit errors:
import PLimit from "p-limit";
const limit = PLimit(5); // 5 concurrent requests
const results = await Promise.all( urls.map((url) => limit(() => client.extract.json({ url, json_schema }))),);from concurrent.futures import ThreadPoolExecutor
# 5 concurrent requestswith ThreadPoolExecutor(max_workers=5) as executor: results = list( executor.map( lambda url: client.extract.json(url=url, json_schema=json_schema), urls, ) )