{"id":78639,"date":"2026-09-20T07:39:04","date_gmt":"2026-09-20T07:39:04","guid":{"rendered":"https:\/\/www.devopsschool.com\/blog\/?p=78639"},"modified":"2026-09-20T07:39:06","modified_gmt":"2026-09-20T07:39:06","slug":"building-resilient-web-data-pipelines-how-to-handle-rate-limits-proxy-failures-and-infrastructure-outages","status":"publish","type":"post","link":"https:\/\/www.devopsschool.com\/blog\/building-resilient-web-data-pipelines-how-to-handle-rate-limits-proxy-failures-and-infrastructure-outages\/","title":{"rendered":"Building Resilient Web Data Pipelines: How to Handle Rate Limits, Proxy Failures and Infrastructure Outages"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A web data collection script can work perfectly during testing and still become unreliable within hours of entering production.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A target website may start returning rate limits. A proxy can become unavailable. Network requests can begin timing out. A browser worker can consume more memory than expected. The pipeline may even report a successful run while collecting only a fraction of the expected data.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That last scenario is particularly dangerous. A process being alive does not mean the data is correct or complete.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Reliable web data pipelines are designed around this reality. They expect individual requests, dependencies, workers, and infrastructure components to fail. The goal is not to eliminate every failure but to isolate failures, recover from them, and keep successful work moving.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Why Web Data Pipelines Fail in Production<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A typical collection pipeline depends on several layers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The target website can introduce rate limits, return HTTP errors, change its page structure, or present a challenge instead of the expected content.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The network layer can fail through connection timeouts, proxy outages, DNS problems, or poor connectivity.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The processing infrastructure introduces another set of risks. Workers can run out of memory, storage can fill up, processes can crash, and scheduling problems can leave jobs stuck or prevent them from running altogether.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">These failures should not be treated identically.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A temporary timeout may justify a retry. Invalid proxy credentials require intervention. A <a href=\"https:\/\/www.devopsschool.com\/blog\/404-2\/\">404 response<\/a> is often a valid result rather than a technical failure. A 429 response indicates that the system needs to respect the target&#8217;s rate limit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This classification determines what happens next.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Handling Rate Limits Without Making Them Worse<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A common failure pattern is to treat every unsuccessful request as a signal to immediately try again.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That can turn a temporary problem into a larger outage.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Suppose a product collection pipeline starts receiving HTTP 429 responses. If hundreds of workers immediately retry those requests, the system can generate another burst of traffic and extend the problem.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A better approach is controlled recovery.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">First, record the failed request and relevant response information. Then classify the failure and determine whether it is safe to retry. Retryable requests should return to a controlled queue rather than blocking the worker.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Exponential backoff increases the delay between successive attempts. Adding jitter prevents many workers from waking up and retrying at exactly the same time.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Retries should also have boundaries. A maximum attempt count prevents one request from consuming resources indefinitely. An overall deadline provides another limit when a dependency remains slow for an extended period.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Concurrency matters as well. <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Glossary\/Domain_sharding\">Per domain concurrency limits<\/a> can prevent one unstable target from consuming capacity needed by other jobs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The objective is not to force every request through but to maintain predictable system behaviour when the target becomes temporarily unavailable.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Isolating Proxy Failures and External Dependencies<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A production collection system should assume that external dependencies will occasionally become unreliable.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Roman Milyushkevich, CEO and CTO at <a href=\"https:\/\/hasdata.com\/\">HasData<\/a>, designs web data collection systems around this assumption.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u201cThe objective is not to make every request succeed. It is to prevent one unreliable website from consuming the resources needed by every other job.\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">His approach starts by separating fetching, validation, and delivery. A failed request goes back into a controlled queue instead of crashing the worker or blocking the entire pipeline.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Roman also recommends classifying failures before retrying. A connection reset or temporary timeout can justify another attempt. Invalid proxy credentials need intervention. A 404 is normally a valid response. A 429 requires respecting the target&#8217;s rate limit instead of immediately sending more requests.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Circuit breakers provide another layer of isolation. If a dependency repeatedly fails, the system can temporarily stop sending work to it rather than wasting worker capacity on requests that are unlikely to succeed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This becomes particularly useful when collecting from multiple domains. One unstable website should not prevent jobs targeting healthy websites from continuing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Use Queues and Idempotent Jobs for Recovery<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Failure isolation becomes much easier when individual collection tasks have their own state.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of treating a collection run as one large operation, break it into independently tracked jobs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A simple architecture looks like this:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>JOB SCHEDULER<\/strong><br><strong>\u2193<\/strong><br>Creates and schedules collection tasks<br>\u2193<br><strong>TASK QUEUE<\/strong><br>\u2193<br>Holds pending tasks and manages retries<br>\u2193<br><strong>COLLECTION WORKERS<\/strong><br>\u2193<br>Fetch and process individual tasks<br>\u2193<br><strong>VALIDATION &amp; STORAGE<\/strong><br>\u2193<br>Validate results and store successful data<br>\u2193<br><strong>MONITORING &amp; RECOVERY<\/strong><br>\u2193<br>Detect failures and incomplete results<br>\u2193<br><strong>FAILED TASKS<\/strong><br>\u2193<br>Return to TASK QUEUE for controlled retry<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The scheduler creates collection tasks. The queue holds pending work and manages retries. Workers process individual tasks. Validation checks whether the returned data is actually usable before storage.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Each task should have a clear state, such as queued, processing, retrying, completed, or dead letter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This makes partial recovery possible. If one worker crashes after processing 500 tasks, the system does not need to restart the entire collection run.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Idempotency is equally important. Retrying a task should not create duplicate records or corrupt existing state. Unique task identifiers, processing status, checkpoints, and appropriate database constraints can help ensure that repeated execution produces a predictable result.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Roman describes the practical advantage this way:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u201cI would rather have 97% of the data delivered on schedule and 3% clearly marked for retry than have the entire run fail because one domain became unstable.\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The 3% remains visible and recoverable rather than disappearing inside a failed batch.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Do Not Confuse HTTP Success With Data Success<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">One of the easiest mistakes in web data collection is treating an HTTP 200 response as proof that the extraction succeeded.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It is not.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A website can return HTTP 200 while serving an empty page, a challenge screen, an unexpected template, or a layout that no longer contains the fields your parser expects.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is why Roman separates transport success from data success.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u201cAfter fetching a response, validate the actual content. Check required fields, expected content, record structure, or other application specific conditions before committing the result.\u201d<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This distinction also improves monitoring. A pipeline that records every HTTP 200 as successful may look healthy while quietly producing incomplete data.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Preventing Hosting and Resource Failures<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Even when request handling is well designed, the infrastructure running the pipeline can become the bottleneck.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mike Kharchenko, Hosting Expert at <a href=\"https:\/\/www.hostzealot.com\/\">HostZealot<\/a>, sees resource contention as a common production problem, particularly when multiple browser based processes run concurrently.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Headless browsers can consume substantial CPU and memory during startup, JavaScript execution, and page rendering. Running many sessions simultaneously can cause memory spikes, swapping, browser crashes, and request timeouts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mike recommends measuring memory consumption per browser context under realistic workloads rather than estimating capacity from worker counts alone.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Other resources deserve attention too.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Browser sessions generate temporary files and maintain network connections. Restrictive process limits can produce connection failures that may initially look like proxy problems. Disk usage, swap activity, open connections, CPU load, and process limits therefore belong in infrastructure monitoring.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Capacity planning should also distinguish between lightweight HTTP workers and browser workers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example, a workload with 100 HTTP workers and 10 browser sessions should not be sized simply by counting 110 workers. The memory consumed by each browser session needs to be measured, along with the headroom required by the operating system and supporting services.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mike uses 90% memory utilization during browser launches as a signal to investigate concurrency limits, unclosed processes, and unusually heavy pages before simply adding more workers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The objective is predictable completion under peak load, not the highest possible worker count.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Observability Must Measure Data, Not Just Servers<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Traditional infrastructure monitoring answers an important question:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Is the system running?&nbsp;<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A reliable data pipeline needs to answer another: <strong>Is the system producing the expected result?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Useful metrics include:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Request success rate:<\/strong> How many requests completed successfully<\/li>\n\n\n\n<li><strong>Retry rate:<\/strong> Frequency of transient failures<\/li>\n\n\n\n<li><strong>Timeout rate:<\/strong> Network or dependency responsiveness<\/li>\n\n\n\n<li><strong>Validation failure rate:<\/strong> Whether returned data meets expectation<\/li>\n\n\n\n<li><strong>Queue backlog:<\/strong> Whether work is accumulating<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Completeness checks are particularly valuable.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Imagine a scheduled job normally collects 10,000 product records but suddenly produces 2,800. The process may still exit normally. Without an expected volume check or validation rule, the pipeline could report success and deliver incomplete data downstream.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Monitoring should therefore track both system health and collection outcomes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>A Practical Recovery Strategy<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A resilient pipeline can use a straightforward recovery sequence:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Detect the failure.<\/strong> Capture errors, timeouts, unexpected responses, or validation failures.<\/li>\n\n\n\n<li><strong>Classify it.<\/strong> Determine whether the failure is temporary, permanent, or requires intervention.<\/li>\n\n\n\n<li><strong>Isolate the task or dependency.<\/strong> Prevent one failure from consuming shared resources.<\/li>\n\n\n\n<li><strong>Retry under controlled conditions.<\/strong> Apply backoff, jitter, concurrency limits, and retry budgets where appropriate.<\/li>\n\n\n\n<li><strong>Record the failure.<\/strong> Preserve enough context to understand what happened.<\/li>\n\n\n\n<li><strong>Validate recovery.<\/strong> Confirm that the resulting data is actually usable.<\/li>\n\n\n\n<li><strong>Escalate unresolved work.<\/strong> Send persistent failures to a dead letter queue or human review process.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The important point is that recovery is not endless repetition. A resilient pipeline knows when to retry and when to stop.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Production Readiness Checklist<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before deploying a web data pipeline, verify that:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Retry logic uses backoff and defined limits.<\/li>\n\n\n\n<li>Request concurrency is controlled.<\/li>\n\n\n\n<li>External dependencies have timeouts.<\/li>\n\n\n\n<li>Failed tasks can be retried independently.<\/li>\n\n\n\n<li>Duplicate processing is prevented.<\/li>\n\n\n\n<li>Job states and progress are recorded.<\/li>\n\n\n\n<li>Returned data is validated before storage.<\/li>\n\n\n\n<li>CPU, memory, disk, and process limits are monitored.<\/li>\n\n\n\n<li>Alerts identify incomplete workloads.<\/li>\n\n\n\n<li>Recovery procedures have been tested.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Reliability Is a Pipeline Property<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Reliable web data collection does not come from adding a retry function or moving the application to a larger server.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It comes from designing the entire pipeline around failure.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">External websites can become unstable. Proxies can fail. Workers can crash. Browser processes can consume unexpected resources. A response can succeed at the HTTP level while failing at the data level. The architecture needs to account for all of these conditions.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A web data collection script can work perfectly during testing and still become unreliable within hours of entering production. A target website may start returning rate limits&#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-78639","post","type-post","status-publish","format-standard","hentry","category-best-tools"],"_links":{"self":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/78639","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=78639"}],"version-history":[{"count":1,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/78639\/revisions"}],"predecessor-version":[{"id":78640,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/78639\/revisions\/78640"}],"wp:attachment":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/media?parent=78639"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/categories?post=78639"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/tags?post=78639"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}