---
title: Production Reliability | Tabstack
description: 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

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

* [TypeScript](#tab-panel-175)
* [Python](#tab-panel-176)
* [curl](#tab-panel-177)

```
// Price monitoring: always fresh
const result = await client.extract.json({
  url: "https://competitor.com/pricing",
  nocache: true,
  json_schema: {
    /* ... */
  },
});
```

```
# Price monitoring, always fresh
result = client.extract.json(
    url="https://competitor.com/pricing",
    nocache=True,
    json_schema={...},
)
```

Terminal window

```
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

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:**

- [TypeScript](#tab-panel-178)
- [Python](#tab-panel-179)

```
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

### 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](/production/error-reference/index.md) 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.

- [TypeScript](#tab-panel-180)
- [Python](#tab-panel-181)

```
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

429 errors are auto-retried, but if you’re hitting them consistently you’re exceeding your [plan’s rate limit](/production/rate-limits/index.md). Use exponential backoff in your own queuing layer for bulk operations:

- [TypeScript](#tab-panel-182)
- [Python](#tab-panel-183)

```
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 time
from 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)
        raise
```

### 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.

- [TypeScript](#tab-panel-184)
- [Python](#tab-panel-185)

```
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

Default timeout is **60 seconds** per request. `effort: 'max'` on complex SPAs can approach this. Tune per endpoint:

- [TypeScript](#tab-panel-186)
- [Python](#tab-panel-187)

```
// Longer timeout for heavy pages
const 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

Serverless platforms cap how long a function can run, and that cap is often shorter than a Tabstack call. If your function’s maximum duration is lower than the call, the platform stops the function before Tabstack responds.

Budget generously. Effort level sets page-fetch time, but an `/extract/json` call against an array schema spends most of its time on extraction and routinely runs past 30 seconds at any effort level, with a long tail beyond two minutes. `/automate` and `/research` can stream for several minutes. See [Effort levels](/guides/effort-levels#effort-is-not-your-whole-response-time/index.md) for measured numbers.

There is no async job mode and no webhook callback. Every endpoint is a single synchronous request, and `/automate` and `/research` stream over SSE, so the connection must stay open for the whole call. That means you cannot escape a platform timeout by submitting work and polling for it later; you have to give the call room to finish, or move it off the request path yourself.

Set the function’s max duration above your expected call time:

- **Vercel:** raise `maxDuration` in the route segment config (or `vercel.json`). The default on Hobby is short, so give `extract` and `generate` headroom for `max` effort, and give `/automate` and `/research` room for the full task.
- **AWS Lambda:** raise the function timeout to cover the call. Lambda’s own ceiling is 15 minutes, which is enough for anything Tabstack does. The problem is usually in front of it: API Gateway applies a 29-second integration timeout that you cannot raise, and it returns a `504` to your caller while the Lambda keeps running. Since raising it is not an option, an `/extract/json` array extraction behind API Gateway needs to move off the request path.

### Running the call off the request path

There is no async or webhook mode to fall back on, so if a call can outlast your platform’s limit, you own the queueing. The shape that works:

1. Your handler validates the request, writes a record with a `pending` status, enqueues the job, and returns `202` immediately with an ID.
2. A worker with a long timeout picks up the job and makes the Tabstack call. On Lambda, that means invoking a second function with `InvocationType: 'Event'` so the caller doesn’t wait, or a queue with an SQS-triggered consumer. On Vercel, a background function or a queue like QStash or Inngest.
3. The worker writes the result back against the ID.
4. Your client polls the ID, or you notify it yourself over websocket, SSE, or a push.

The worker is where the generous SDK timeouts matter: leave them alone and let the call finish rather than capping it below the work.

If the call comfortably fits inside your function’s limit, skip all of this and call Tabstack directly. This is for array extractions, `max` effort on heavy pages, and the streaming endpoints.

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](/guides/streaming-patterns/index.md) for edge-runtime examples.

---

## Logging

Enable debug logging during development to see full request/response detail:

- [TypeScript](#tab-panel-188)
- [Python](#tab-panel-189)

```
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

For high-volume extraction jobs (many URLs), control concurrency to avoid rate limit errors:

- [TypeScript](#tab-panel-190)
- [Python](#tab-panel-191)

```
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 requests
with ThreadPoolExecutor(max_workers=5) as executor:
    results = list(
        executor.map(
            lambda url: client.extract.json(url=url, json_schema=json_schema),
            urls,
        )
    )
```
