You configured a rotating proxy, you send ten requests, and every one comes back reporting the same exit address. The obvious conclusion is that rotation is broken. It usually is not. In most cases the gateway is handing out fresh addresses exactly as asked and something between your code and it is preventing that from happening, and the most common culprit is a feature of your HTTP client that exists to make things faster.
Here are the causes in the order worth checking, with the fix for each.
First, check it properly
Before diagnosing, make sure the test itself is sound. A single command line request per invocation is the cleanest check, because each run is a separate process with no shared state:
for i in 1 2 3 4 5; do
curl -s -x customer-USERNAME:PASSWORD@p.shifter.io:443 https://ipinfo.io/json \
| python3 -c "import sys,json; print(json.load(sys.stdin)['ip'])"
done
Five different addresses means rotation works and your application is the problem. Five identical ones means the configuration is. That single distinction saves most of the debugging time, so run it first.
Cause one: a session identifier in the username
The simplest explanation. If your username contains a sid flag, you have explicitly asked for a sticky session, and holding the address is the correct behaviour rather than a fault.
customer-USERNAME-country-us-sid-abc123 # sticky: same IP by design
customer-USERNAME-country-us # rotating: fresh IP per request
This catches people who copied an example from documentation, or who built the username with a helper that adds a session by default. Remove the sid flag, and the ttl with it since TTL is only meaningful alongside a session. The semantics are in sticky versus rotating and the sessions docs.
A subtler version: your session identifier is constant when you meant it to vary. If you generate one per job but the job runs many requests, every request in that job shares an address, which is correct but may not be what you intended.
Cause two: connection reuse, the one that catches everyone
This is the answer most of the time when the curl loop above rotates and your code does not.
Modern HTTP clients keep connections alive and reuse them, because opening a new TCP and TLS connection for every request is slow. When your client reuses an existing tunnel to the proxy, the request travels through the connection that is already established, and that connection already has an exit address attached. Rotation happens per connection, not per request sent down an existing one, so a session object that keeps a connection pool will faithfully send every request through the same exit.
The fix depends on how much control you want. Either disable keep-alive, or force a new connection per request, or make each logical request use a fresh client.
import requests
PROXY = "http://customer-USERNAME:PASSWORD@p.shifter.io:443"
PROXIES = {"http": PROXY, "https": PROXY}
# Reuses one connection: same exit address for every request
s = requests.Session()
for _ in range(5):
print(s.get("https://ipinfo.io/json", proxies=PROXIES).json()["ip"])
# Fresh connection per request: rotates as expected
for _ in range(5):
r = requests.get("https://ipinfo.io/json", proxies=PROXIES,
headers={"Connection": "close"}, timeout=20)
print(r.json()["ip"])
The same applies elsewhere: an http.Agent with keep-alive in Node, a shared HttpClient in .NET or Java, a connection pool in Go. If your language has a default client that pools connections, and they all do, this is the first thing to check in application code.
Worth saying plainly: this is a trade-off rather than a bug. Connection reuse is faster and cheaper. If your work genuinely wants a new address per request, you pay for a new connection each time; if it does not, reuse is fine and often preferable.
Cause three: you are checking too quickly, or the pool is smaller than you think for that filter
Two related effects.
If you have narrowed the filter tightly, to a small city or a specific ASN, the set of addresses that can serve you is much smaller than the pool overall, so the same address legitimately reappears more often. That is not a failure, it is arithmetic. Broaden the filter and the repetition drops away.
Also remember that a residential pool is made of real connections that come and go, so seeing an address twice across many requests is expected rather than suspicious. Rotation means the next request is independently selected, not that an address can never recur. If you need guaranteed distinctness for a workload, that is a design constraint to handle in your own code.
Cause four: something upstream is caching
If you are testing through a browser, an extension, a system-level proxy setting, or a corporate network, the request may not be going where you think. Browsers in particular hold connections open aggressively and reuse them across tabs, so a browser is the worst environment for verifying rotation. Test from the command line first, then in your application, and only then in a browser.
Similarly, if your code sets proxy environment variables as well as passing configuration explicitly, one may be overriding the other and sending traffic through something other than the gateway you configured.
Cause five: the address is the same but the request never left
A blunt one worth ruling out: if the proxy is not actually being used, every request reports the same address, namely yours. Confirm that the address you are seeing is not your own server’s address. If it is, the proxy configuration is not being applied at all, which is a different problem and usually a missing https entry alongside the http one, or a client that ignores the proxy setting for the scheme you are using.
Cause six: a sticky session that has not expired yet
If you are deliberately using sticky sessions and expecting them to rotate after a while, remember that the address holds until the TTL expires. Default lifetime is 120 seconds unless you set ttl explicitly. If you want a new address sooner, change the session identifier rather than waiting, since a new identifier means a new session.
Note also that a sticky address can drop earlier than its TTL if the underlying connection goes away, since these are real home connections rather than dedicated infrastructure. Sticky means best effort for the requested duration, not a guarantee.
A quick decision path
Run the curl loop. If it rotates and your code does not, you have a connection reuse problem, which is cause two. If neither rotates, check the username for a sid flag, then confirm you are not seeing your own address, then broaden any narrow geo filter. If it rotates less often than you would like but does rotate, you are looking at pool size for that filter rather than a fault.
For everything else, the surrounding behaviour is documented in how rotation works, and if requests are failing rather than repeating, why requests time out and fixing 407 errors cover the two most common failure modes.
The bottom line
Rotation problems are usually not rotation problems. Test with separate processes first to establish whether the gateway is rotating at all, and if it is, look at your HTTP client, because connection reuse is the overwhelming favourite: requests sent down an already-open tunnel keep the exit address that tunnel already has. After that, check for a stray sid flag, confirm you are not looking at your own address, and remember that a very narrow geo filter draws from a much smaller set so repeats are normal. Sticky sessions holding their address are working as designed, and changing the identifier gives you a new one immediately.
The rotation model itself, and the flags that control it, sit on the residential proxy network, where session behaviour is a parameter per request rather than a plan setting, billed per GB regardless of how often you rotate.