Something in your pipeline just started returning 403s, or a challenge page, or an empty 200 where there used to be JSON. The reflex — in every tutorial, every Stack Overflow answer, every proxy vendor's docs — is to rotate the IP.
Most of the time that's the wrong first move, and it's expensive in a way that isn't obvious: rotating throws away everything the old identity had accumulated. Session cookies, a solved challenge, whatever internal trust score the target had assigned you after 40 well-behaved requests. If the IP wasn't the thing that got flagged, you paid all of that and fixed nothing — and you now have a second identity showing the same pattern, which is a worse signal than the first one alone.
The useful question isn't "how do I get unblocked." It's what scope did the block apply to. There are five, they're cheap to tell apart, and the right response is different for each.
The five scopes
Request-scoped. One specific request was rejected — a missing header, a bad referer, an expired token in a query param, a URL that requires a POST. The next request works fine. This is the most common "block" and it isn't a block at all.
Session-scoped. Your cookie jar / TLS session got flagged. A challenge interstitial that keeps re-serving, a cf_clearance that went stale, a rate limiter keyed on session ID. Everything else about you is fine.
Fingerprint-scoped. Your client is being rejected for what it is, not where it's from. Automation-flavoured TLS handshake, a headless-shaped JS environment, a header order no real browser emits. The tell: it fails instantly and identically no matter how long you wait or what IP you use.
IP-scoped. The address is on a list. Every request from it fails, from every session, with every client, immediately.
Account-scoped. You're logged in and the account is limited. Rotating the IP here is actively harmful — a logged-in account that suddenly appears from a new city is a much stronger abuse signal than one that stays put.
Four probes that separate them
You don't need to guess. Four requests, under a minute, and the answer falls out:
- Same IP, fresh session (new cookie jar, same client, same exit).
-
Same IP, different client (
curlif you were using a browser, or vice versa — this varies the TLS/JA3 and header order). - Different IP, same cookies.
- Different IP, everything fresh.
import httpx
def probe(url, *, proxy=None, cookies=None, headers=None, http2=True):
"""One probe. Returns (status, len(body), challenged)."""
with httpx.Client(proxy=proxy, cookies=cookies or {},
headers=headers or {}, http2=http2,
timeout=20, follow_redirects=True) as c:
r = c.get(url)
body = r.text
challenged = any(s in body.lower() for s in
("just a moment", "un momento", "checking your browser",
"attention required", "verify you are human"))
return r.status_code, len(body), challenged
def diagnose(url, old_proxy, new_proxy, old_cookies, browser_headers):
p = {}
p["fresh_session_same_ip"] = probe(url, proxy=old_proxy, headers=browser_headers)
p["diff_client_same_ip"] = probe(url, proxy=old_proxy, http2=False, headers={})
p["old_cookies_new_ip"] = probe(url, proxy=new_proxy, cookies=old_cookies,
headers=browser_headers)
p["all_fresh_new_ip"] = probe(url, proxy=new_proxy, headers=browser_headers)
return p
Read it like this:
| What happens | Scope | What to actually change |
|---|---|---|
| Fresh session on the same IP works | Session | Drop the cookie jar. Keep the IP. |
| Same IP fails, new IP with old cookies also fails | Session or account | Cookies. Not the IP. |
Everything fails instantly from both IPs, but a plain curl works |
Fingerprint | Your client, not your route. |
| Every request from the old IP fails, everything from the new one works | IP | Rotate — this is the one case where it's right. |
| Only some URLs fail, others are fine | Request | Fix the headers/method/token. |
| Fails only while logged in | Account | Slow down. Do not move the account to a new IP. |
The row people get wrong is the second one. A stale session on a target that keys its rate limiter to a session ID looks exactly like an IP ban from the inside — every request fails — and rotating the IP "fixes" it, because the new exit also comes with a new cookie jar. So you conclude the IP was burned when the cookie was, and you burn an address per incident forever after.
The waiting case
There's a fifth outcome the probes will show you and most people won't believe: temporary, and the correct action is nothing.
I hit this on Medium's Cloudflare edge a couple of weeks ago. Mid-session, after an import that touched several endpoints in quick succession, every page turned into an interstitial. Same tab, same everything, for something like half an hour — reloading did nothing, four attempts did nothing. The exit IP was verifiably unchanged the whole time (I checked, precisely because rotating was the tempting move and that session had a login I didn't want to re-establish). Then it cleared on its own and the next request went through normally.
If I'd rotated at minute two I would have concluded the IP was blocked, thrown away a good address and a live session, and learned the wrong lesson permanently. The signal that it was temporary was there: the challenge was serving (200 with a challenge body), not refusing (403 with nothing). A challenge is an invitation to prove yourself. A 403 with an empty body is a door. They deserve opposite responses.
Change the cheapest thing first
Order your remediations by what they cost you if you're wrong:
- Headers / request shape — free.
- Cookie jar — cheap, unless you're logged in.
- Wait — costs only time, and time is what half of these need.
- Client fingerprint — moderate; you're rebuilding a client, not an identity.
- Exit IP — expensive. You lose the session, the accumulated trust, and (if you were logged in) you hand the target a "this account moved cities" event.
- Account — most expensive, obviously.
Rotation belongs at position five for a reason. It's the loudest change you can make and it's the one people reach for first, because it's the one the tooling makes easiest.
A practical version of this: keep the diagnostic in your codebase, not in your head. When a run starts failing, diagnose() runs before any remediation logic does, logs which scope it found, and only the IP-scoped branch is allowed to call rotate(). It takes an afternoon to write and it stops the reflex from making decisions for you.
We publish code examples and testing notes for developers who scrape and automate at RoamProxy. More runnable examples: github.com/roamproxy/proxy-examples.
Top comments (0)