1.A scraper has several timeout layers
Start by naming each clock. HTTPX separates connect, read, write, and pool timeouts in its official timeout documentation. A connect timeout covers socket establishment. A read timeout covers periods without a response chunk, not necessarily total request duration. A pool timeout means all client connections are busy. Treating those exceptions as the same event sends engineers toward the wrong fix.
A managed scraping request adds more clocks. The outer HTTP client waits for OmniScrape, the scrape request's timeout bounds work on the target, and js_wait_timeout limits how long a rendered page waits for a selector. Your worker or queue usually has a final job deadline as well. The outer deadline must leave enough time for the inner work to finish and for the response body to cross the network.
- Connect timeout: a TCP or TLS connection was not established in time.
- Read timeout: no response data arrived within the client's read window.
- Write timeout: the client could not send request data within its limit.
- Pool timeout: concurrency exhausted the client's available connections.
- Scrape timeout: the retrieval attempt exceeded its processing budget.
- Render wait timeout: the expected DOM signal never appeared.
- Job deadline: the queue worker exhausted the total budget for every attempt.
2.Diagnose the expired clock before changing it
Log the exception class, elapsed time, attempt number, target hostname, scrape mode, and response status when one exists. A PoolTimeout with growing concurrency points to local connection starvation; increasing the target timeout cannot fix it. A fast 429 is not a timeout at all. It is an explicit rate-limit response that should reduce request pressure.
A read timeout after a long wait is ambiguous: the target may still be processing, the scrape service may be finishing a render, or the response path may have stalled. Repeating it immediately can duplicate work. A render selector timeout is different again. The selector may be wrong, the page may show an empty state, consent may block the view, or a challenge page may have replaced the expected content.
- Connect failures across many domains: inspect DNS, egress, TLS, and proxy health.
- Connect failures on one domain: inspect target reachability and regional routing.
- Pool timeouts: lower worker concurrency or increase a measured connection-pool limit.
- Read timeouts only in browser mode: inspect render duration and outer-client headroom.
- Selector timeouts: save the rendered HTML or screenshot and verify the readiness signal.
- 429 or 503 responses: honor Retry-After when present and reduce retry pressure.
3.Build a deadline budget from the outside in
Choose a total job deadline first, based on how long the data can remain stale and how quickly the queue must recover. Divide that budget among attempts, backoff, and persistence. A job with a two-minute deadline cannot safely make three 60-second attempts plus sleeps. Either reduce the attempt count, shorten the inner work, or give the worker a larger explicit deadline.
Keep a small margin between nested clocks so the inner layer can return a useful error before the outer client disconnects. For example, a scrape processing budget can be shorter than the HTTP read window, and a selector wait can be shorter than the scrape processing budget. The values are workload decisions, not universal defaults: measure p50, p95, and failure-tail latency per domain and mode before tuning them.
- Job deadline = attempts + all backoff delays + response validation + persistence.
- Client read window should exceed the intended inner scrape budget by a margin.
- Selector wait should leave time for capture, extraction, and response serialization.
- Fast HTTP and JavaScript-rendered routes should have separate latency profiles.
- Never disable every timeout in a background worker; stalled sockets then consume capacity indefinitely.
4.Use explicit HTTPX timeouts and a bounded retry budget
The example below uses a safe public W3C page, explicit HTTPX timeout classes, a monotonic job deadline, and full-jitter backoff. It retries transport timeouts and a short allowlist of transient status codes. It does not retry malformed requests, authentication failures, or application responses that say extraction failed.
The OmniScrape scrape API reference is the source of truth for current request fields. Keep your API key in an environment variable. A read timeout is an unknown-completion event, so downstream writes should be idempotent: upsert using a stable job or record key rather than blindly inserting again after a retry.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869import os
import random
import time
import httpx
API_URL = "https://api.omniscrape.io/v1/scrape"
RETRYABLE_STATUS = {429, 502, 503, 504}
def scrape_with_deadline(url: str, deadline_seconds: float = 150) -> dict:
deadline = time.monotonic() + deadline_seconds
timeout = httpx.Timeout(connect=10, read=80, write=10, pool=5)
limits = httpx.Limits(max_connections=20, max_keepalive_connections=10)
payload = {
"url": url,
"mode": "auto",
"output_format": "html",
"timeout": 60,
}
with httpx.Client(
headers={"X-API-Key": os.environ["OMNISCRAPE_KEY"]},
timeout=timeout,
limits=limits,
) as client:
for attempt in range(3):
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("Scrape job deadline exhausted")
try:
response = client.post(API_URL, json=payload)
except (httpx.ConnectTimeout, httpx.ReadTimeout, httpx.PoolTimeout):
if attempt == 2:
raise
delay = random.uniform(0, min(2 ** attempt, 8))
if delay >= remaining:
raise TimeoutError("No retry time remains")
time.sleep(delay)
continue
else:
if response.status_code not in RETRYABLE_STATUS:
response.raise_for_status()
body = response.json()
if not body.get("success"):
raise RuntimeError(body.get("error", "scrape failed"))
return body
retry_after = response.headers.get("Retry-After", "")
if retry_after.isdigit():
delay = float(retry_after)
else:
delay = random.uniform(0, min(2 ** attempt, 8))
if delay >= remaining or attempt == 2:
response.raise_for_status()
time.sleep(delay)
raise RuntimeError("Retry budget exhausted")
result = scrape_with_deadline(
"https://www.w3.org/WAI/ARIA/apg/patterns/table/examples/table/"
)
html = result["data"]["content"]
if "<table" not in html.lower():
raise ValueError("Expected table content was not returned")
5.Retry only failures that can plausibly change
Retries help when a connection is temporarily unavailable, a service returns a transient 5xx response, or a rate-limit window will reopen. They do not repair an invalid API key, malformed payload, wrong selector, blocked access policy, or missing page. Repeating permanent failures consumes the same deadline and can amplify load on both your worker and the target.
When a response includes Retry-After, follow it within your job budget. The HTTP specification allows that field to contain either a delay in seconds or an HTTP date; production parsers should support both forms. The HTTP Semantics standard defines the field. Add jitter to locally calculated delays so a fleet of workers does not wake and retry at the same instant.
Set a maximum attempt count and a maximum elapsed time. The elapsed-time cap is the stronger guarantee because a single slow attempt can consume most of the job. Once the retry budget is exhausted, move the item to a delayed queue or dead-letter path with its diagnostic context rather than looping inside the same worker forever.
6.Treat rendering and page readiness separately
For JavaScript pages, use a selector that proves the required data is ready, not a fixed sleep. OmniScrape's Web Unlocker documentation documents js_wait_selector and js_wait_timeout for rendered requests. A catalog container may exist before its cards arrive, so wait for a stable row, price, or results marker instead of the outer shell.
Fixed sleeps make fast pages wait unnecessarily and still fail on unusually slow pages. Playwright likewise discourages page.waitForTimeout in production and recommends signals such as locators or network events in its Page API documentation. When the workflow requires clicking, pagination, or state across several navigations, use Browser-as-a-Service over CDP and assign separate navigation and action limits rather than one global timer.
1234567891011{
"url": "https://public.example/catalog",
"mode": "js_rendering",
"output_format": "css_extractor",
"js_wait_selector": "[data-testid='product-card']",
"js_wait_timeout": 12000,
"css_selectors": {
"title": "[data-testid='product-card'] h2",
"price": "[data-testid='product-card'] .price"
}
}
7.Timeouts can be a concurrency problem
If latency rises as workers scale, the target may not be the cause. Requests can queue behind an undersized local connection pool, CPU-heavy parsing, browser capacity, or a downstream database. Pool timeouts are especially useful because they reveal that the client could not obtain a connection; raising the read timeout would only keep occupied connections around longer.
Measure successful records per minute rather than requests started per second. Reduce concurrency when timeout and 429 rates rise together. Cap work per domain so one slow host cannot occupy every worker, and reserve capacity for retries instead of letting retry traffic compete without limits. The HTTPX scraping guide shows bounded asynchronous concurrency, while the data pipeline guide covers queues and dead-letter handling.
8.A completed request still needs content validation
No timeout and HTTP 200 do not prove the intended page arrived. A challenge page, consent screen, empty application shell, or target error template can complete quickly. Validate required selectors or fields, a plausible body size, the final URL, and a page-specific invariant before saving records. Keep timeout metrics separate from extraction-quality metrics so improvements to one cannot conceal regressions in the other.
Save a small diagnostic artifact for failures: status code, final URL, elapsed time by stage, response length, expected-selector result, and a redacted HTML sample or screenshot. Avoid logging credentials, cookies, personal data, or full URLs containing sensitive query parameters.
9.Production timeout checklist
When protected-page retrieval is the slow or unreliable layer, OmniScrape Web Unlocker can consolidate proxy routing, rendering, and structured extraction behind one request. Your application should still own its total deadline, retry policy, validation, and idempotent writes.
Collect only data you are authorized to use. Respect applicable law, website terms, access controls, privacy obligations, robots directives where applicable, and rate limits. A timeout is not permission to increase pressure on a struggling service.
- Name and log each timeout layer instead of recording one generic failure.
- Set an explicit total job deadline and make every inner clock fit inside it.
- Use separate profiles for fast HTTP and JavaScript rendering.
- Retry only transient failures, honor Retry-After, add jitter, and cap elapsed time.
- Wait for meaningful page signals instead of fixed sleeps.
- Limit concurrency per domain and align the HTTP connection pool with worker count.
- Validate content after transport success and quarantine unexpected output.
- Make downstream writes idempotent because timeout completion can be ambiguous.
Frequently asked questions
What timeout should I use for web scraping?
There is no universal value. Measure latency by target and mode, choose a total job deadline, and fit connect, read, scrape, render-wait, retries, and persistence inside it. Keep browser-rendered pages in a separate profile from fast HTTP pages.
What is the difference between a connect timeout and a read timeout?
A connect timeout expires while establishing the network connection. A read timeout expires when response data does not arrive within the configured read window. They point to different bottlenecks and should be logged separately.
Should I retry every scraping timeout?
No. Retry a bounded number of transient transport failures. Do not retry invalid credentials, malformed payloads, missing pages, wrong selectors, or access-policy failures without changing the underlying condition.
Why does JavaScript rendering time out even though the page loads?
The readiness selector may be wrong, data may load only after interaction, a consent or challenge page may be present, or the target may be slow. Save the rendered output and verify the selector before increasing js_wait_timeout.
Can more concurrency cause scraping timeouts?
Yes. Workers can exhaust the local connection pool, overload parsing or storage, trigger target rate limits, or saturate browser capacity. Compare pool wait, request latency, 429 rate, and successful records per minute as concurrency changes.
Related guides
- Web Scraping API: Endpoint, Modes, Output Formats & Integration Patterns
- HTTPX Web Scraping: Async Python with OmniScrape
- Scrape JavaScript-Rendered Pages: SPAs, Hydration, and Hidden APIs
- Web Scraping Data Pipeline: Queue, Validate, and Store
- Remote Browsers over CDP with Playwright and Puppeteer
- Web Scraping Pagination: URLs, Buttons, and Infinite Scroll