Rotation fires per TCP connection, not per request. Keep-alive holds the socket, so the exit never moves. Reproduce it in 30 seconds, then fix by client.
· 11 min read · Troubleshooting
A rotating proxy not changing IP is almost never a broken rotating proxy. In the overwhelming majority of cases the gateway rotated exactly as designed and your HTTP client never gave it the chance, because rotation fires when a new TCP connection is established and your client is reusing one it opened minutes ago. HTTP keep-alive is on by default in most modern clients. Keep-alive holds the socket. The socket holds the exit address.
Reproduce that in 30 seconds before you read another word of theory, then work down the list.
Two loops. Same credentials, same port, same target. One rotates. One does not.
# A - new connection per request. The exit address changes.
import requests
for _ in range(5):
r = requests.get(
"https://api.ipify.org/",
proxies={"https": "http://login:[email protected]:823"},
)
print(r.text)
# B - one pooled connection, reused. The exit address does not change.
import requests
s = requests.Session()
s.proxies = {"https": "http://login:[email protected]:823"}
for _ in range(5):
print(s.get("https://api.ipify.org/").text)
Loop A prints five addresses. Loop B prints one address five times. Nothing differs between them except who owns the TCP connection. If your production job looks like B and your test looked like A, you have found your bug and you can stop reading here.
The gateway assigns an exit address when a connection is established, and it does not reassign mid-connection. There is no mechanism by which it could — by the time your second request goes out, the tunnel to the exit is already built, and rewriting the source address of a live TCP stream is not something a proxy does.
For an HTTPS target this is even more definitive than it sounds. Your client
sends CONNECT api.example.com:443 to gw.anonedge.com:823, the gateway
selects an exit, and everything after that is opaque bytes flowing through a
tunnel the gateway cannot inspect or re-route. The exit is fixed at CONNECT
time. Five hundred requests can ride that tunnel and every one of them leaves
from the same address.
So the operative rule is short: anything that keeps the socket to the gateway open keeps the exit address. Anything that tears it down gets you a new one.
That framing also explains the symptom that confuses people most — rotation that "starts working" after a pause. Idle connections get reaped on a timer. Sit still long enough and the pool drops the socket, the next request opens a fresh one, and the address moves. Intermittent rotation is not flaky rotation. It is your connection pool's idle timeout.
Every mainstream client pools by default now. Named specifics:
requests. A Session mounts an HTTPAdapter backed by
urllib3 connection pools. Pooling is the entire point of the object.
requests.get() at module level creates and disposes a session per call,
which is why loop A rotates and loop B does not.http / https, and therefore axios. Since Node.js 19 the global
agent sets keepAlive: true by default with a 5-second keep-alive duration,
per the Node.js 19 release
announcement.
Axios uses that global agent unless you hand it your own, so on any current
runtime your axios calls are pooled whether you asked for it or not.undici, and therefore Node's global fetch. undici pools
aggressively by design and exposes the behavior through connections,
keepAliveTimeout and pipelining on the dispatcher. Default settings reuse.curl inside one invocation. Multiple URLs in a single curl command
share a connection. Separate curl processes cannot.None of these are misconfigurations. They are correct defaults for the workload they were designed for, which is many requests to one origin. They are simply the wrong defaults for a workload whose entire premise is many origins' worth of source addresses.
Pick the row for your stack. Every fix does the same thing — forces a new TCP connection to the gateway — and they differ only in how much of your throughput they cost.
| Client | Fix | Notes |
|---|---|---|
Python requests |
Use requests.get() per call, or a fresh Session() per request |
Simplest and most reliable. Costs a TCP + TLS handshake per request |
Python requests (keep the session) |
session.headers["Connection"] = "close" |
Signals teardown after the response, so the pooled socket is discarded |
httpx |
Construct a new Client per identity, or set limits=httpx.Limits(max_keepalive_connections=0) |
Disables the keep-alive pool while leaving the client reusable |
axios |
Pass an explicit agent: new http.Agent({ keepAlive: false }) and new https.Agent({ keepAlive: false }) |
Overrides the Node 19+ global default. See the Axios agent settings |
undici / global fetch |
Dispatcher with pipelining: 0, or a short keepAliveTimeout |
pipelining: 0 disables connection reuse |
| Scrapy | Send Connection: close in per-request headers, and set meta["proxy"] per request |
Scrapy's Twisted downloader maintains a persistent pool otherwise |
| Go | &http.Transport{Proxy: ..., DisableKeepAlives: true} |
One field. Do not share a Transport across identities |
curl |
One process per request, or --no-keepalive |
Separate processes cannot share a pool |
One caveat on all of them: you are deliberately giving up connection reuse, so you pay a TCP handshake and a TLS handshake on every request. That is the actual price of per-request rotation and it is unavoidable. If the cost hurts, the answer is usually not to re-enable pooling — it is to ask whether the job needed per-request rotation in the first place. Many do not. See rotating vs sticky proxy sessions for that decision.
Keep-alive accounts for most of these tickets. When it does not, work down this list in order.
Ports 10000 through 20000 are sticky by design. Each port number is its own session slot and holds one exit address for the length of the window. If your config says 10000 and you expected rotation, the network is doing exactly what you asked. Rotating traffic goes to 823 for HTTP/HTTPS and 824 for SOCKS5. The full mapping is in rotating and sticky ports, and the port range itself is documented under the 10000-20000 sticky range.
This is the number one cause among people who have already ruled out keep-alive, and it is usually a config file that was edited for one job and never reverted.
You are on a sticky port intentionally, but the window is longer than your test. The window runs from 1 to 120 minutes with a 30-minute default, so a five-minute test against a default window will show one address five times and prove nothing. Either shorten the window in your session configuration or test on a rotating port instead.
The address changed. The response did not. CDN edges, geo-redirect layers and anything that sets a location cookie will happily serve you the previous region's content from cache for minutes after your exit moved. If you are inferring your exit address from what a target renders, you are measuring the target's cache, not the gateway. Always confirm against a plain IP echo.
Some test endpoints report X-Forwarded-For, X-Real-IP or a via chain
rather than the socket peer. Those headers are set by intermediaries and can be
stale, forged, or simply describing a different hop than the one you care about.
Use an endpoint that returns the connecting address and nothing else.
A system proxy, a corporate egress gateway, a VPN client, an antivirus TLS
inspection layer, or a local debugging proxy such as mitmproxy or Charles will
intercept before your traffic ever reaches gw.anonedge.com. Symptoms: an exit
address that belongs to your own organization, or one that never changes no
matter what you do. Check HTTP_PROXY, HTTPS_PROXY, ALL_PROXY and
NO_PROXY in the environment your process actually runs in, which on a
container or CI runner is frequently not the environment you edited.
Long-lived processes resolve gw.anonedge.com once and cache the answer for the
life of the process. Combined with a pool, that means one resolved host, one
socket, one exit — indefinitely. Restarting the process is the crude fix;
disabling keep-alive is the real one.
Five separate processes, one plain IP echo, nothing clever:
for i in $(seq 1 5); do
curl -s -x "http://login:[email protected]:823" https://api.ipify.org/
echo
done
Each iteration is a distinct curl process, so there is no pool to accidentally
share. Five distinct addresses means rotation is healthy end to end. Five
identical addresses from five separate processes means the problem is not your
client, and you should be looking at causes 1, 2, 5 or 6 above.
Two things to be honest about. Five samples is a spot check, not a measurement — a pool can return an address you have already seen without anything being wrong, so occasional repeats across a larger sample are expected rather than diagnostic. And a stable address proves only that the exit did not move. It proves nothing about whether your target can see through the setup.
This is a different article's problem, and misdiagnosing it as a rotation problem burns days.
If five separate processes return five distinct addresses and your target still blocks you, the address was not what identified you. The usual suspects, roughly in order: TLS and HTTP/2 fingerprints that do not match the browser your User-Agent claims to be, cookies and local storage carried across identities, account-level linkage that survives any IP change, and header ordering that no real browser emits. Rotating faster makes none of those better and some of them worse.
Two error codes are worth recognizing before you go looking, both documented under proxy error codes:
400 NO_RAY — no exit address matched your targeting filter. This is a
targeting problem masquerading as a connection problem. Widen the filter one
level at a time; when ASN targeting earns the 2x
rate covers which level to drop first.407 THREADS_EXHAUSTED — you hit the concurrent connection ceiling on
your plan. Disabling keep-alive raises connection churn, so it is not unusual
to fix rotation and immediately meet this instead.If the fingerprint rather than the address is the issue, headless browser vs HTTP requests is where that decision belongs.
Almost always HTTP keep-alive. Rotation is assigned when a TCP connection is
established, so a client that reuses one pooled connection keeps the exit
address it was given on the first request. Python's requests.Session, axios on
Node 19 and later, and undici all pool by default. Force a new connection per
request and the address moves.
Every connection. That distinction is the entire source of the confusion. Ports 823 and 824 assign a new exit each time a connection is established to the gateway, so per-request rotation only happens if your client actually opens a connection per request. For HTTPS targets the exit is fixed at CONNECT time and cannot change for the life of the tunnel.
Run five separate curl processes against a plain IP echo endpoint such as
api.ipify.org. Separate processes cannot share a connection pool, which
removes the most common false negative. Five distinct addresses means rotation
works. Never test against your target — CDN and geo caching can return the old
region's content after the exit has already changed.
Yes, measurably. You pay a TCP handshake and a TLS handshake per request instead of amortizing them across a pooled connection. That is the real cost of per-request rotation. Before paying it, check whether the job needs a new address per request at all — most authenticated flows need the opposite, and most independent fetches tolerate rotation per small batch.
The address was not what identified you. Look at TLS and HTTP/2 fingerprints, header order, cookies carried across identities, and account-level linkage. All four survive an IP change. Rotating harder is the wrong lever and can make detection easier, because a stable fingerprint hopping across many addresses is itself a pattern.
Your terminal spawns a new process per command and your application does not. The application holds a long-lived client with a warm connection pool, so it keeps whichever exit it was assigned at startup. Reproduce the application's behavior with a scripted loop that reuses one session object, and you will see the same single address the application sees.
Work the list in order and the fix is usually one line. Disable keep-alive, confirm with five separate processes against an IP echo, then check the port number before you check anything else.
If five distinct processes still return one address, the problem is upstream of your code. Talk to support with the port, the timestamps and the addresses you observed — those three things resolve it faster than any description of the symptom.