Respecting robots.txt and Crawl-Delay: Ethical Scraping That Still Scales


The Scraper
Proxy Fundamentals
Here is the uncomfortable truth about robots.txt: it can't stop you. It's a text file at a known URL. Your scraper can fetch it, ignore it, and keep going, the server has no technical way to enforce a single line, and in most jurisdictions it carries no direct legal weight on its own.
Which is exactly the point. Because nothing forces you to honor it, whether you do is the clearest signal of whether you're a professional running a sustainable operation or a problem waiting to get blocked. Abuse teams notice the difference.
This post is about reading and respecting robots.txt and crawl-rate signals correctly, without turning your crawler into something that takes a week to do an hour's work. Politeness and throughput are not opposites; done right, the polite crawler is the one still running next month.
What robots.txt Actually Is
The Robots Exclusion Protocol (REP) started as an informal convention in 1994 and was written down as RFC 9309 in 2022. It's a voluntary standard: a compliant crawler reads the file and obeys it, assuming good faith on the client side.
A file is a set of groups. Each starts with one or more User-agent lines, followed by Allow and Disallow rules:
User-agent: * Disallow: /cart/ Disallow: /search Allow: /search/help User-agent: BadBot Disallow: / Sitemap: https://example.com/sitemap.xml Crawl-delay: 5
The pieces:
User-agentnames which crawler the group applies to.*is the default for any agent without a more specific match.Disallowlists path prefixes the agent should not fetch. An emptyDisallow:means "nothing is blocked."Allowcarves exceptions out of a broaderDisallow(standardized in RFC 9309).Sitemappoints to one or more sitemap URLs. It's global, not tied to a group.Crawl-delayasks for N seconds between requests. This is not part of RFC 9309, it's a widely-deployed extension some crawlers honor and others ignore.
What robots.txt is not, and this matters:
It is not access control. A
Disallowrequires no authentication and blocks nothing. If you don't want a page fetched, don't serve it publicly.It is not law. It's not a contract you agreed to. (A site's Terms of Service are a separate question, see below.)
It is not a security boundary. Treating it as one is a server-side mistake, not a scraper's permission slip.
So why honor something purely advisory? Because the directives encode what the operator wants automated traffic to avoid, usually endpoints that are expensive, useless to you, or sensitive. Respecting them keeps you out of trouble and off the slow paths.
Reading It Correctly
Most "robots.txt bugs" are misreadings, not parse failures.
Match the right user-agent group. A crawler picks the single most specific group whose User-agent token is a case-insensitive prefix match of its own product token. If you call yourself MyCrawler and there's a group for MyCrawler and one for *, you obey MyCrawler and ignore * entirely, groups do not merge or inherit. Fall back to * only if nothing more specific matches.
Longest match wins. When both an Allow and a Disallow could apply, the rule with the longest matching path wins. For the file above, /search/help is allowed (longer Allow) even though /search is disallowed. Equal-length ties resolve in favor of Allow.
Wildcards. RFC 9309 supports * (any sequence) and $ (end of URL): Disallow: /*?sort= blocks any URL with that query fragment; Disallow: /*.pdf$ blocks PDFs.
Treat the Sitemap line as a gift. It's a structured index of canonical URLs — exactly what you'd otherwise crawl blindly to discover. Using it means fewer wasted requests: more polite and more efficient. Always check for it.
Parsing It in Python
The standard library ships urllib.robotparser, zero-dependency and fine for simple cases.
from urllib.robotparser import RobotFileParser rp = RobotFileParser() rp.set_url("https://example.com/robots.txt") rp.read() # fetches and parses UA = "MyCrawler/1.0" print(rp.can_fetch(UA, "https://example.com/search/help")) # True print(rp.can_fetch(UA, "https://example.com/cart/")) # False # crawl_delay() returns the value if present (Python 3.6+), else None print(rp.crawl_delay(UA))
The standard library has real limits, though: no built-in refresh policy, historically weaker wildcard handling than RFC 9309 requires, and crawl_delay() support that depends on your Python version. For anything serious, reach for a dedicated library.
Protego is the parser Scrapy uses, and it tracks RFC 9309 closely (wildcards, longest-match, the lot):
import httpx from protego import Protego UA = "MyCrawler/1.0" text = httpx.get("https://example.com/robots.txt").text rp = Protego.parse(text) print(rp.can_fetch("https://example.com/search/help", UA)) # True print(rp.crawl_delay(UA)) # e.g. 5.0 print(list(rp.sitemaps)) # discovery for free
reppy is another option focused on performance, though it has historically lagged on maintenance. For most projects: protego for correctness, the stdlib for zero dependencies.
Every parser leaves one thing to you: fetch robots.txt once per host, cache it, and refresh periodically (a few hours is typical). Handle the edge cases too, a 404 conventionally means "everything allowed," while a 5xx or timeout is best treated as "back off, the site is having a bad time."
Crawl-Delay and Choosing a Polite Rate
Honoring Crawl-delay is the easy part: if the file asks for 5 seconds, your per-host scheduler waits at least 5 seconds between requests to that host. When there's no directive (the common case), you pick a default. Sane starting points:
1 request per second per host, a reasonable conservative default for most sites.
One every 2-5 seconds for small sites, single-server setups, or anything that feels hand-built.
Faster only with evidence the host can take it, large CDN-backed platforms absorb far more, but let response times tell you, not optimism.
The number that matters is requests per host, not your global rate. That's the single most important idea here.
Be adaptive. A fixed delay is a guess; the server's responses are data. Back off when you see trouble:
import time def polite_delay(state, response, base_delay): if response.status_code == 429 or response.status_code >= 500: # exponential backoff, capped state["delay"] = min(state["delay"] * 2, 60) else: # ease back toward the baseline on success state["delay"] = max(base_delay, state["delay"] * 0.9) time.sleep(state["delay"]) return state
A 429 Too Many Requests (honor any Retry-After header) or a wave of 503s is the server explicitly telling you to slow down. Ignoring it is how you go from "rate-limited" to "permanently blocked."
Cap per-host concurrency. Politeness isn't only about delay between requests — it's also about not opening 50 parallel connections to one origin. One or two concurrent per host is plenty. Scale by adding more hosts, not more pressure on each one.
Where Proxy Rotation Fits
This is the part people get wrong. Rotating IPs, datacenter, ISP, or residential, spreads requests across addresses so no single IP looks like a firehose. That's legitimate for distributing load and avoiding IP-reputation false positives.
But rotation is not a license to hammer. Fire 100 requests/second at one host across 100 IPs and you're still putting 100 requests/second of real load on their infrastructure. IP diversity hides you from naive per-IP rate limits, but it doesn't change the load you impose, and behavioral detection still flags the aggregate pattern. Per-host pacing has to live in your scheduler, independent of how many IPs you rotate through. A clean pool, say, residential IPs like Evomi's, keeps you off shared deny-lists, but the pacing discipline is yours to enforce.
The Business Case for Being Polite
If the ethics don't move you, the economics should. Polite crawlers:
Get blocked less. Aggressive patterns are the number-one trigger for rate limits, IP bans, and CAPTCHA walls. Slow down and you trip fewer of them, and every challenge you provoke costs a solver call, a retry, or a dead request.
Keep long-term access. Most data work needs a stable feed over months, not one heroic scrape before a ban. Sustainable rate beats peak rate.
Lower legal and PR risk. Staying off the "unusual traffic" radar keeps a scrape out of a complaint, and out of a headline.
Politeness is also just better engineering. Per-host scheduling, backoff, caching, and sitemap-driven discovery are the same primitives that make a crawler robust, how you survive flaky networks and slow origins regardless of etiquette.
The Legal and Ethical Landscape, Briefly
This is informational, not legal advice, talk to a lawyer for your case. The broad strokes:
Public vs. authenticated data is the biggest line. Scraping behind a login, paywall, or "click to agree" wall is a meaningfully different risk profile than fetching public pages.
Terms of Service can bind you in ways
robots.txtdoesn't, particularly if you accepted them by creating an account.Personal data (GDPR, CCPA, and similar) doesn't care whether data was public. Collecting and storing PII carries obligations regardless of how you got it.
The case law moves. The long hiQ Labs v. LinkedIn arc, where scraping public profiles was found not to violate the US Computer Fraud and Abuse Act, even as other claims survived, narrowed CFAA's reach for public data but didn't make scraping consequence-free. Treat any single case as a data point, not a green light.
When in doubt, check for an official API first. It's often cheaper, more stable, and explicitly permitted.
A Good-Citizen Checklist
Identify yourself honestly in your User-Agent where appropriate, a product name and contact URL let an operator reach you instead of blocking you.
Cache aggressively. Don't re-fetch what hasn't changed; honor
ETagandLast-Modified.Request only what you need. Skip assets, tracking endpoints, and out-of-scope pages. Use the sitemap.
Crawl off-peak for the target's timezone when you can.
Honor every signal:
robots.txt,Crawl-delay,429/Retry-After, and5xxbackoff.Ask first when it's high-volume. A short email requesting access (or an API key) often beats any amount of clever evasion.
Wrapping Up
robots.txt can't make you do anything, and that's the whole point, honoring it marks you as someone worth granting access to. Parse it correctly (protego for RFC 9309 fidelity, the stdlib for zero dependencies), cache it per host, and read the Sitemap line as the free discovery index it is.
Then put the discipline where it counts: per-host pacing in your scheduler, adaptive backoff on 429/5xx, and a per-host concurrency cap that proxy rotation never overrides. That single habit, pacing per host, not per IP, is what lets a crawler be both polite and fast, and what keeps it running long after the impatient ones get banned.
Here is the uncomfortable truth about robots.txt: it can't stop you. It's a text file at a known URL. Your scraper can fetch it, ignore it, and keep going, the server has no technical way to enforce a single line, and in most jurisdictions it carries no direct legal weight on its own.
Which is exactly the point. Because nothing forces you to honor it, whether you do is the clearest signal of whether you're a professional running a sustainable operation or a problem waiting to get blocked. Abuse teams notice the difference.
This post is about reading and respecting robots.txt and crawl-rate signals correctly, without turning your crawler into something that takes a week to do an hour's work. Politeness and throughput are not opposites; done right, the polite crawler is the one still running next month.
What robots.txt Actually Is
The Robots Exclusion Protocol (REP) started as an informal convention in 1994 and was written down as RFC 9309 in 2022. It's a voluntary standard: a compliant crawler reads the file and obeys it, assuming good faith on the client side.
A file is a set of groups. Each starts with one or more User-agent lines, followed by Allow and Disallow rules:
User-agent: * Disallow: /cart/ Disallow: /search Allow: /search/help User-agent: BadBot Disallow: / Sitemap: https://example.com/sitemap.xml Crawl-delay: 5
The pieces:
User-agentnames which crawler the group applies to.*is the default for any agent without a more specific match.Disallowlists path prefixes the agent should not fetch. An emptyDisallow:means "nothing is blocked."Allowcarves exceptions out of a broaderDisallow(standardized in RFC 9309).Sitemappoints to one or more sitemap URLs. It's global, not tied to a group.Crawl-delayasks for N seconds between requests. This is not part of RFC 9309, it's a widely-deployed extension some crawlers honor and others ignore.
What robots.txt is not, and this matters:
It is not access control. A
Disallowrequires no authentication and blocks nothing. If you don't want a page fetched, don't serve it publicly.It is not law. It's not a contract you agreed to. (A site's Terms of Service are a separate question, see below.)
It is not a security boundary. Treating it as one is a server-side mistake, not a scraper's permission slip.
So why honor something purely advisory? Because the directives encode what the operator wants automated traffic to avoid, usually endpoints that are expensive, useless to you, or sensitive. Respecting them keeps you out of trouble and off the slow paths.
Reading It Correctly
Most "robots.txt bugs" are misreadings, not parse failures.
Match the right user-agent group. A crawler picks the single most specific group whose User-agent token is a case-insensitive prefix match of its own product token. If you call yourself MyCrawler and there's a group for MyCrawler and one for *, you obey MyCrawler and ignore * entirely, groups do not merge or inherit. Fall back to * only if nothing more specific matches.
Longest match wins. When both an Allow and a Disallow could apply, the rule with the longest matching path wins. For the file above, /search/help is allowed (longer Allow) even though /search is disallowed. Equal-length ties resolve in favor of Allow.
Wildcards. RFC 9309 supports * (any sequence) and $ (end of URL): Disallow: /*?sort= blocks any URL with that query fragment; Disallow: /*.pdf$ blocks PDFs.
Treat the Sitemap line as a gift. It's a structured index of canonical URLs — exactly what you'd otherwise crawl blindly to discover. Using it means fewer wasted requests: more polite and more efficient. Always check for it.
Parsing It in Python
The standard library ships urllib.robotparser, zero-dependency and fine for simple cases.
from urllib.robotparser import RobotFileParser rp = RobotFileParser() rp.set_url("https://example.com/robots.txt") rp.read() # fetches and parses UA = "MyCrawler/1.0" print(rp.can_fetch(UA, "https://example.com/search/help")) # True print(rp.can_fetch(UA, "https://example.com/cart/")) # False # crawl_delay() returns the value if present (Python 3.6+), else None print(rp.crawl_delay(UA))
The standard library has real limits, though: no built-in refresh policy, historically weaker wildcard handling than RFC 9309 requires, and crawl_delay() support that depends on your Python version. For anything serious, reach for a dedicated library.
Protego is the parser Scrapy uses, and it tracks RFC 9309 closely (wildcards, longest-match, the lot):
import httpx from protego import Protego UA = "MyCrawler/1.0" text = httpx.get("https://example.com/robots.txt").text rp = Protego.parse(text) print(rp.can_fetch("https://example.com/search/help", UA)) # True print(rp.crawl_delay(UA)) # e.g. 5.0 print(list(rp.sitemaps)) # discovery for free
reppy is another option focused on performance, though it has historically lagged on maintenance. For most projects: protego for correctness, the stdlib for zero dependencies.
Every parser leaves one thing to you: fetch robots.txt once per host, cache it, and refresh periodically (a few hours is typical). Handle the edge cases too, a 404 conventionally means "everything allowed," while a 5xx or timeout is best treated as "back off, the site is having a bad time."
Crawl-Delay and Choosing a Polite Rate
Honoring Crawl-delay is the easy part: if the file asks for 5 seconds, your per-host scheduler waits at least 5 seconds between requests to that host. When there's no directive (the common case), you pick a default. Sane starting points:
1 request per second per host, a reasonable conservative default for most sites.
One every 2-5 seconds for small sites, single-server setups, or anything that feels hand-built.
Faster only with evidence the host can take it, large CDN-backed platforms absorb far more, but let response times tell you, not optimism.
The number that matters is requests per host, not your global rate. That's the single most important idea here.
Be adaptive. A fixed delay is a guess; the server's responses are data. Back off when you see trouble:
import time def polite_delay(state, response, base_delay): if response.status_code == 429 or response.status_code >= 500: # exponential backoff, capped state["delay"] = min(state["delay"] * 2, 60) else: # ease back toward the baseline on success state["delay"] = max(base_delay, state["delay"] * 0.9) time.sleep(state["delay"]) return state
A 429 Too Many Requests (honor any Retry-After header) or a wave of 503s is the server explicitly telling you to slow down. Ignoring it is how you go from "rate-limited" to "permanently blocked."
Cap per-host concurrency. Politeness isn't only about delay between requests — it's also about not opening 50 parallel connections to one origin. One or two concurrent per host is plenty. Scale by adding more hosts, not more pressure on each one.
Where Proxy Rotation Fits
This is the part people get wrong. Rotating IPs, datacenter, ISP, or residential, spreads requests across addresses so no single IP looks like a firehose. That's legitimate for distributing load and avoiding IP-reputation false positives.
But rotation is not a license to hammer. Fire 100 requests/second at one host across 100 IPs and you're still putting 100 requests/second of real load on their infrastructure. IP diversity hides you from naive per-IP rate limits, but it doesn't change the load you impose, and behavioral detection still flags the aggregate pattern. Per-host pacing has to live in your scheduler, independent of how many IPs you rotate through. A clean pool, say, residential IPs like Evomi's, keeps you off shared deny-lists, but the pacing discipline is yours to enforce.
The Business Case for Being Polite
If the ethics don't move you, the economics should. Polite crawlers:
Get blocked less. Aggressive patterns are the number-one trigger for rate limits, IP bans, and CAPTCHA walls. Slow down and you trip fewer of them, and every challenge you provoke costs a solver call, a retry, or a dead request.
Keep long-term access. Most data work needs a stable feed over months, not one heroic scrape before a ban. Sustainable rate beats peak rate.
Lower legal and PR risk. Staying off the "unusual traffic" radar keeps a scrape out of a complaint, and out of a headline.
Politeness is also just better engineering. Per-host scheduling, backoff, caching, and sitemap-driven discovery are the same primitives that make a crawler robust, how you survive flaky networks and slow origins regardless of etiquette.
The Legal and Ethical Landscape, Briefly
This is informational, not legal advice, talk to a lawyer for your case. The broad strokes:
Public vs. authenticated data is the biggest line. Scraping behind a login, paywall, or "click to agree" wall is a meaningfully different risk profile than fetching public pages.
Terms of Service can bind you in ways
robots.txtdoesn't, particularly if you accepted them by creating an account.Personal data (GDPR, CCPA, and similar) doesn't care whether data was public. Collecting and storing PII carries obligations regardless of how you got it.
The case law moves. The long hiQ Labs v. LinkedIn arc, where scraping public profiles was found not to violate the US Computer Fraud and Abuse Act, even as other claims survived, narrowed CFAA's reach for public data but didn't make scraping consequence-free. Treat any single case as a data point, not a green light.
When in doubt, check for an official API first. It's often cheaper, more stable, and explicitly permitted.
A Good-Citizen Checklist
Identify yourself honestly in your User-Agent where appropriate, a product name and contact URL let an operator reach you instead of blocking you.
Cache aggressively. Don't re-fetch what hasn't changed; honor
ETagandLast-Modified.Request only what you need. Skip assets, tracking endpoints, and out-of-scope pages. Use the sitemap.
Crawl off-peak for the target's timezone when you can.
Honor every signal:
robots.txt,Crawl-delay,429/Retry-After, and5xxbackoff.Ask first when it's high-volume. A short email requesting access (or an API key) often beats any amount of clever evasion.
Wrapping Up
robots.txt can't make you do anything, and that's the whole point, honoring it marks you as someone worth granting access to. Parse it correctly (protego for RFC 9309 fidelity, the stdlib for zero dependencies), cache it per host, and read the Sitemap line as the free discovery index it is.
Then put the discipline where it counts: per-host pacing in your scheduler, adaptive backoff on 429/5xx, and a per-host concurrency cap that proxy rotation never overrides. That single habit, pacing per host, not per IP, is what lets a crawler be both polite and fast, and what keeps it running long after the impatient ones get banned.
Here is the uncomfortable truth about robots.txt: it can't stop you. It's a text file at a known URL. Your scraper can fetch it, ignore it, and keep going, the server has no technical way to enforce a single line, and in most jurisdictions it carries no direct legal weight on its own.
Which is exactly the point. Because nothing forces you to honor it, whether you do is the clearest signal of whether you're a professional running a sustainable operation or a problem waiting to get blocked. Abuse teams notice the difference.
This post is about reading and respecting robots.txt and crawl-rate signals correctly, without turning your crawler into something that takes a week to do an hour's work. Politeness and throughput are not opposites; done right, the polite crawler is the one still running next month.
What robots.txt Actually Is
The Robots Exclusion Protocol (REP) started as an informal convention in 1994 and was written down as RFC 9309 in 2022. It's a voluntary standard: a compliant crawler reads the file and obeys it, assuming good faith on the client side.
A file is a set of groups. Each starts with one or more User-agent lines, followed by Allow and Disallow rules:
User-agent: * Disallow: /cart/ Disallow: /search Allow: /search/help User-agent: BadBot Disallow: / Sitemap: https://example.com/sitemap.xml Crawl-delay: 5
The pieces:
User-agentnames which crawler the group applies to.*is the default for any agent without a more specific match.Disallowlists path prefixes the agent should not fetch. An emptyDisallow:means "nothing is blocked."Allowcarves exceptions out of a broaderDisallow(standardized in RFC 9309).Sitemappoints to one or more sitemap URLs. It's global, not tied to a group.Crawl-delayasks for N seconds between requests. This is not part of RFC 9309, it's a widely-deployed extension some crawlers honor and others ignore.
What robots.txt is not, and this matters:
It is not access control. A
Disallowrequires no authentication and blocks nothing. If you don't want a page fetched, don't serve it publicly.It is not law. It's not a contract you agreed to. (A site's Terms of Service are a separate question, see below.)
It is not a security boundary. Treating it as one is a server-side mistake, not a scraper's permission slip.
So why honor something purely advisory? Because the directives encode what the operator wants automated traffic to avoid, usually endpoints that are expensive, useless to you, or sensitive. Respecting them keeps you out of trouble and off the slow paths.
Reading It Correctly
Most "robots.txt bugs" are misreadings, not parse failures.
Match the right user-agent group. A crawler picks the single most specific group whose User-agent token is a case-insensitive prefix match of its own product token. If you call yourself MyCrawler and there's a group for MyCrawler and one for *, you obey MyCrawler and ignore * entirely, groups do not merge or inherit. Fall back to * only if nothing more specific matches.
Longest match wins. When both an Allow and a Disallow could apply, the rule with the longest matching path wins. For the file above, /search/help is allowed (longer Allow) even though /search is disallowed. Equal-length ties resolve in favor of Allow.
Wildcards. RFC 9309 supports * (any sequence) and $ (end of URL): Disallow: /*?sort= blocks any URL with that query fragment; Disallow: /*.pdf$ blocks PDFs.
Treat the Sitemap line as a gift. It's a structured index of canonical URLs — exactly what you'd otherwise crawl blindly to discover. Using it means fewer wasted requests: more polite and more efficient. Always check for it.
Parsing It in Python
The standard library ships urllib.robotparser, zero-dependency and fine for simple cases.
from urllib.robotparser import RobotFileParser rp = RobotFileParser() rp.set_url("https://example.com/robots.txt") rp.read() # fetches and parses UA = "MyCrawler/1.0" print(rp.can_fetch(UA, "https://example.com/search/help")) # True print(rp.can_fetch(UA, "https://example.com/cart/")) # False # crawl_delay() returns the value if present (Python 3.6+), else None print(rp.crawl_delay(UA))
The standard library has real limits, though: no built-in refresh policy, historically weaker wildcard handling than RFC 9309 requires, and crawl_delay() support that depends on your Python version. For anything serious, reach for a dedicated library.
Protego is the parser Scrapy uses, and it tracks RFC 9309 closely (wildcards, longest-match, the lot):
import httpx from protego import Protego UA = "MyCrawler/1.0" text = httpx.get("https://example.com/robots.txt").text rp = Protego.parse(text) print(rp.can_fetch("https://example.com/search/help", UA)) # True print(rp.crawl_delay(UA)) # e.g. 5.0 print(list(rp.sitemaps)) # discovery for free
reppy is another option focused on performance, though it has historically lagged on maintenance. For most projects: protego for correctness, the stdlib for zero dependencies.
Every parser leaves one thing to you: fetch robots.txt once per host, cache it, and refresh periodically (a few hours is typical). Handle the edge cases too, a 404 conventionally means "everything allowed," while a 5xx or timeout is best treated as "back off, the site is having a bad time."
Crawl-Delay and Choosing a Polite Rate
Honoring Crawl-delay is the easy part: if the file asks for 5 seconds, your per-host scheduler waits at least 5 seconds between requests to that host. When there's no directive (the common case), you pick a default. Sane starting points:
1 request per second per host, a reasonable conservative default for most sites.
One every 2-5 seconds for small sites, single-server setups, or anything that feels hand-built.
Faster only with evidence the host can take it, large CDN-backed platforms absorb far more, but let response times tell you, not optimism.
The number that matters is requests per host, not your global rate. That's the single most important idea here.
Be adaptive. A fixed delay is a guess; the server's responses are data. Back off when you see trouble:
import time def polite_delay(state, response, base_delay): if response.status_code == 429 or response.status_code >= 500: # exponential backoff, capped state["delay"] = min(state["delay"] * 2, 60) else: # ease back toward the baseline on success state["delay"] = max(base_delay, state["delay"] * 0.9) time.sleep(state["delay"]) return state
A 429 Too Many Requests (honor any Retry-After header) or a wave of 503s is the server explicitly telling you to slow down. Ignoring it is how you go from "rate-limited" to "permanently blocked."
Cap per-host concurrency. Politeness isn't only about delay between requests — it's also about not opening 50 parallel connections to one origin. One or two concurrent per host is plenty. Scale by adding more hosts, not more pressure on each one.
Where Proxy Rotation Fits
This is the part people get wrong. Rotating IPs, datacenter, ISP, or residential, spreads requests across addresses so no single IP looks like a firehose. That's legitimate for distributing load and avoiding IP-reputation false positives.
But rotation is not a license to hammer. Fire 100 requests/second at one host across 100 IPs and you're still putting 100 requests/second of real load on their infrastructure. IP diversity hides you from naive per-IP rate limits, but it doesn't change the load you impose, and behavioral detection still flags the aggregate pattern. Per-host pacing has to live in your scheduler, independent of how many IPs you rotate through. A clean pool, say, residential IPs like Evomi's, keeps you off shared deny-lists, but the pacing discipline is yours to enforce.
The Business Case for Being Polite
If the ethics don't move you, the economics should. Polite crawlers:
Get blocked less. Aggressive patterns are the number-one trigger for rate limits, IP bans, and CAPTCHA walls. Slow down and you trip fewer of them, and every challenge you provoke costs a solver call, a retry, or a dead request.
Keep long-term access. Most data work needs a stable feed over months, not one heroic scrape before a ban. Sustainable rate beats peak rate.
Lower legal and PR risk. Staying off the "unusual traffic" radar keeps a scrape out of a complaint, and out of a headline.
Politeness is also just better engineering. Per-host scheduling, backoff, caching, and sitemap-driven discovery are the same primitives that make a crawler robust, how you survive flaky networks and slow origins regardless of etiquette.
The Legal and Ethical Landscape, Briefly
This is informational, not legal advice, talk to a lawyer for your case. The broad strokes:
Public vs. authenticated data is the biggest line. Scraping behind a login, paywall, or "click to agree" wall is a meaningfully different risk profile than fetching public pages.
Terms of Service can bind you in ways
robots.txtdoesn't, particularly if you accepted them by creating an account.Personal data (GDPR, CCPA, and similar) doesn't care whether data was public. Collecting and storing PII carries obligations regardless of how you got it.
The case law moves. The long hiQ Labs v. LinkedIn arc, where scraping public profiles was found not to violate the US Computer Fraud and Abuse Act, even as other claims survived, narrowed CFAA's reach for public data but didn't make scraping consequence-free. Treat any single case as a data point, not a green light.
When in doubt, check for an official API first. It's often cheaper, more stable, and explicitly permitted.
A Good-Citizen Checklist
Identify yourself honestly in your User-Agent where appropriate, a product name and contact URL let an operator reach you instead of blocking you.
Cache aggressively. Don't re-fetch what hasn't changed; honor
ETagandLast-Modified.Request only what you need. Skip assets, tracking endpoints, and out-of-scope pages. Use the sitemap.
Crawl off-peak for the target's timezone when you can.
Honor every signal:
robots.txt,Crawl-delay,429/Retry-After, and5xxbackoff.Ask first when it's high-volume. A short email requesting access (or an API key) often beats any amount of clever evasion.
Wrapping Up
robots.txt can't make you do anything, and that's the whole point, honoring it marks you as someone worth granting access to. Parse it correctly (protego for RFC 9309 fidelity, the stdlib for zero dependencies), cache it per host, and read the Sitemap line as the free discovery index it is.
Then put the discipline where it counts: per-host pacing in your scheduler, adaptive backoff on 429/5xx, and a per-host concurrency cap that proxy rotation never overrides. That single habit, pacing per host, not per IP, is what lets a crawler be both polite and fast, and what keeps it running long after the impatient ones get banned.

Author
The Scraper
Engineer and Webscraping Specialist
About Author
The Scraper is a software engineer and web scraping specialist, focused on building production-grade data extraction systems. His work centers on large-scale crawling, anti-bot evasion, proxy infrastructure, and browser automation. He writes about real-world scraping failures, silent data corruption, and systems that operate at scale.



