{"id":79478,"date":"2026-10-23T00:17:19","date_gmt":"2026-10-23T00:17:19","guid":{"rendered":"https:\/\/www.devopsschool.com\/blog\/?p=79478"},"modified":"2026-10-23T00:17:21","modified_gmt":"2026-10-23T00:17:21","slug":"designing-a-production-ready-web-scraping-pipeline-that-survives-blocks-retries-and-ci","status":"publish","type":"post","link":"https:\/\/www.devopsschool.com\/blog\/designing-a-production-ready-web-scraping-pipeline-that-survives-blocks-retries-and-ci\/","title":{"rendered":"Designing a production-ready web scraping pipeline that survives blocks, retries, and CI"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Most scraping failures in production look like random network noise. The real cause is usually a weak contract between your code and the target site. You deploy a crawler, it runs fine in a lab, and then it collapses under rate limits, bot checks, or subtle HTML changes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A DevOps approach fixes this by treating web data collection as a service. You define inputs and outputs, add control points, and instrument the parts that fail. You also separate \u201cfetch\u201d from \u201cparse\u201d so you can change routing, headers, and IP strategy without rewriting business logic.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Define the contract: what success looks like<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Start by writing a narrow definition of success for one target workflow. Pick one page type and one extraction output. Define acceptable latency, acceptable missing fields, and the minimum number of pages you must collect per run.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Next, name your failure modes. A 403 block needs a different response than a 404, a timeout, or a changed DOM. Your pipeline should record which class of failure happened, not just \u201crequest failed.\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Finally, decide what \u201cretry\u201d means. Blind retries waste bandwidth and raise your block rate. A retry should change something, like headers, session, route, or delay.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Build a fetch layer you can swap without touching parsers<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Split your code into a fetcher and a parser. The fetcher owns URL building, headers, cookies, TLS settings, proxy routing, and backoff. The parser only sees bytes and returns structured data plus parse errors.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wrap the fetcher behind a clean interface. In Python, that might be a class with fetch(url, context). In Go, it might be an interface that returns status, body, and timing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Store raw responses for a small sample. Keep the HTML for blocked pages, too. That corpus becomes your regression test set when a target changes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Operational checks with curl before you blame your code<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Engineers often debug scrapers inside a runtime that hides details. That slows you down when a WAF starts responding differently. Use curl to reproduce the same request path outside your app.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Recreate the exact headers your fetcher sends, including Accept-Language and User-Agent. Validate redirects and cookies with -L and -c. Compare the status code and response size against a known good run.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you need a practical reference, Byteful is a UK-based web data infrastructure platform offering ethically sourced proxies for web scraping and AI agents, plus API-first tools, and their guide can help you <a href=\"https:\/\/byteful.com\/blog\/curl-download-file\">download a file with curl<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once curl reproduces the issue, capture a minimal replay bundle. Save the request headers, the URL, and the raw response body. Attach that bundle to the incident ticket so others can reproduce fast.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Proxy routing without guesswork<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Proxy use should be a controlled variable, not a magic fix. Choose a proxy type based on what the target validates. Some sites care about IP reputation, others care about session consistency, and some flag impossible geo patterns.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Make routing a policy in configuration. Your policy should decide when to rotate IPs, when to keep a sticky session, and when to change regions. Tie the policy to signals like 429 spikes, repeated 403s, or captcha pages.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keep a \u201cclean path\u201d for diagnostics. When a run fails, you should test direct egress, then the proxy path, with the same request shape. That tells you if the failure comes from your client, your route, or the target.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Instrument the pipeline like any other production job<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Scraping jobs need the same telemetry as CI workloads. Emit structured logs for URL, status, retry count, route, and parse outcome. Add timings for DNS, connect, TLS, and first byte when your client supports it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Set alerts on ratios, not raw counts. Watch 2xx rate, 3xx rate, 4xx rate, and parse success rate. A stable 200 rate with a collapsing parse rate usually means the HTML changed, not that you got blocked.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Build a quarantine path for suspicious data. If fields drop to empty or values shift format, store the output but prevent it from reaching downstream systems. That protects search indexing, pricing engines, and model features.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Compliance and audit trail as first-class features<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Data collection creates risk when teams cannot explain how they collected data. Keep an audit record of what targets you access, why you access them, and how you respect site rules and legal constraints. Store timestamps, request volumes, and retention periods.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Handle personal data explicitly. If a page can include user identifiers, treat it as sensitive input. Apply minimization at the parser stage and redact early.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Choose ethically sourced infrastructure when you need proxies. Byteful manages residential, mobile, static residential (ISP), and datacenter proxies through a single API, and it is a member of the Internet Watch Foundation and the Ethical Web Data Collection Initiative.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Turn it into a runbook your team can execute<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Document the exact steps to validate a failure. Include a curl replay recipe, the logs to pull, and the thresholds that trigger policy changes. Keep the runbook close to the code, and review it after each incident.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Promote changes through the same gates as application code. Add a test that runs fetch plus parse against your stored corpus. If the DOM changed, your CI should catch it before production does.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Most scraping failures in production look like random network noise. The real cause is usually a weak contract between your code and the target site. You deploy&#8230; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_joinchat":[],"footnotes":""},"categories":[11138],"tags":[],"class_list":["post-79478","post","type-post","status-publish","format-standard","hentry","category-best-tools"],"_links":{"self":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/79478","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/comments?post=79478"}],"version-history":[{"count":1,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/79478\/revisions"}],"predecessor-version":[{"id":79479,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/79478\/revisions\/79479"}],"wp:attachment":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/media?parent=79478"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/categories?post=79478"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/tags?post=79478"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}