Your proxy is rotating. Your HTTP client is not.
Two questions arrive here every week. This pair came from one customer, weeks apart, and neither is a beginner’s mistake:
“so i have a set number of connections for each port. Are those connections all different IPs?”
The second, after he had changed plans:
“how is it possible that i made 5 requests for every port every 1 minute for 24h, totally made 3340 requests, and get only 270 unique ips?”
Both are looking at a proxy that is working correctly and reading it as broken, for two different reasons. One is about connections. One is about a clock. Neither is really about proxies at all.
Rotation happens on connections, not requests
Here is the actual sequence. Your client opens a TCP connection to the proxy endpoint. For an HTTPS target it sends CONNECT, and a tunnel is established through to the destination. An exit address is chosen at that moment, and it belongs to that tunnel.
Every request you then write into that tunnel leaves from the same place. One request or nine hundred — it makes no difference. The tunnel already has an address.
An exit address is a property of the tunnel, not of the request.
Which gives you the diagnosis. If you are seeing one address across hundreds of requests, the most likely explanation is not that rotation is broken, or that you were sold something different from what you thought. It is that you have made one connection.
Your HTTP client is doing this on purpose
Connection reuse is not a bug and not an obscure setting. It is the default. It is called keep-alive, and it exists because opening a TCP connection and completing a TLS handshake is genuinely expensive — a couple of round trips before you have sent a single useful byte. Every mature HTTP client avoids paying that repeatedly.
- In Python,
requests.Session()keeps a connection pool, and so doeshttpx.Client. Create the session once and loop, and you are reusing one connection. - In Node 19 and later the global agent ships with keep-alive on and a five-second idle timeout, so
axiosorfetchwith no agent of your own pools by default. The inversion catches people: a hand-rollednew http.Agent()still defaults tokeepAlive: false, so supplying your own agent can switch pooling off unless you ask for it. If you use a proxy-agent library, check which way its own default points. On Node 18 and earlier, nothing pooled unless you turned it on. - In Go,
http.DefaultTransportpools aggressively and will happily serve an entire run from a single connection. - A browser holds connections open per origin and reuses them across a page’s requests, subject to its own idle timeout. That is why a headless browser session, or an antidetect profile, tends to sit on one address.
There is an uncomfortable inversion here. The better your HTTP setup, the more it reuses connections, and the more fixed your exit address will look. Somebody who wrote a naive script that constructs a fresh client on every call sees rotation immediately. Somebody who built something efficient does not.
That is why this gets misread as a product defect by exactly the people least likely to be making a simple mistake.
Our fuller notes on client lifecycle are in a resilient Python setup, and the browser case is covered in headless browsers and proxies.
The thirty-second test
You do not have to take any of this on faith, and you can run it against any provider, including ours. It is the same two requests in two different shapes. In the first, curl fetches two URLs inside one process and reuses the tunnel. In the second, two separate processes each open their own. Substitute your own credentials, endpoint and port.
# One process, one connection — curl reuses the tunnel for both fetches
curl -sS -w '\n' -x "http://USERNAME:PASSWORD@HOST:PORT" \
https://api.ipify.org https://api.ipify.org
# Two processes, two connections
curl -sS -w '\n' -x "http://USERNAME:PASSWORD@HOST:PORT" https://api.ipify.org
curl -sS -w '\n' -x "http://USERNAME:PASSWORD@HOST:PORT" https://api.ipify.org
The -w flag matters — without it the addresses print end to end with no separator and you cannot tell one from two.
Read the result like this:
- First command gives the same address twice, second gives two different addresses. Rotation is per connection, and your client was the variable all along.
- Both commands give the same address twice. Your endpoint is not rotating per connection. It is holding an address on a timer, and you have not waited out the interval. That changes the advice — keep reading.
- First command gives two different addresses. Your client did not reuse the connection. Some clients and some builds will not, and you can stop here.
- One of the fetches errors out. A failed tunnel makes curl open a new connection to retry, which looks like rotation that was not there. Re-run before you read anything into it.
The obvious caveat on the endpoint: api.ipify.org is fine here because you are comparing two answers against each other, not measuring anything. It is a poor tool for almost every other question.
Making it rotate on purpose
Three ways to get a new address on a per-connection endpoint, in rough order of what they cost you.
Open a new connection. A fresh client per unit of work — not a fresh request on a shared client. In Python that means a new Session or Client per item, or a with block per item. In Go it means a new Transport, not a new Request — and call CloseIdleConnections() when that unit of work is done. The standard library expects Transports to be long-lived and caches connections inside them, so one created per item and dropped will hold sockets open until they time out.
Refuse keep-alive. Send Connection: close, or set your client’s pool size to zero. Blunt and effective, and you pay for a TCP and TLS handshake on every single request.
Change the session tag. Where the endpoint takes a session parameter in the username, a new tag is a new identity. Usually the cleanest option, because it expresses what you want rather than fighting your own transport layer. The syntax is in the sessions documentation, and rotating versus sticky sessions covers when you want the opposite.
And then the case where none of those work. If your endpoint rotates on an interval, the address belongs to the port, not to your connection. Port-based plans — ours included — work this way: one port carries one address at a time, shared by every connection through it, and it changes only when that port’s rotation interval elapses. Opening new connections will not move you. Refusing keep-alive will not move you. There are two different controls there: shorten the port’s rotation interval for more turnover over time, or spread the work across more ports, because each port is its own address. This is the single most expensive misunderstanding in this whole subject, and it is why the first bullet of the test above is worth thirty seconds of your time before you change any code.
A concession, because this cuts against the obvious advice: per-request rotation is not free. Every new connection is a handshake, and handshakes cost latency and real bytes. On a metered per-GB plan those bytes land on the invoice and never appear in your library’s byte counter, as how proxy bandwidth is measured explains. On a port-priced or speed-tiered plan they cost you throughput and wall-clock time instead of money. Either way, rotating on every request when the job did not need it makes the job slower. Rotate at the granularity the work actually has.
The other half: connections are not addresses
The same confusion arrives from the other direction, and this version costs money at checkout rather than at runtime.
A customer once bought a port-based plan believing each port carried its own distinct address for every connection through it, found at the terminal that it did not, and asked the same day to move to a different product. He was not misreading the dashboard. He was reading one word — connections — as an answer to a question it does not answer.
A per-port concurrency allowance is a ceiling on how many things can be in flight at once. It says nothing about how many distinct addresses those things land on. That distinction has a post of its own — dedicated ports versus dedicated IPs — and it is worth ten minutes before you buy anything.
Configuration sometimes helps: on a port-based endpoint, shortening a port’s rotation interval raises how many distinct addresses you see over time. What it cannot do is raise how many you hold at once. For that you need more ports, or an endpoint that rotates per connection.
Before you buy anything, write your two numbers down separately: peak simultaneous connections, and distinct addresses required per hour. Then match each to the control that governs it. How many proxies do you need has the concurrency arithmetic.
Two dials, and they have to agree
Rotation granularity is set in two places, and both have to point the same way.
The endpoint has a granularity. Some rotate on every new connection. Some hold an address for a fixed interval, so a new connection opened inside that window returns the same address it did a moment ago. Some hand you the control directly through a session tag. All three are correct behaviour, and the second is what produces “3,340 requests, 270 unique IPs” — interval rotation puts a ceiling on distinct addresses that your request volume cannot raise. Five ports each holding an address for half an hour give you at most a few hundred distinct addresses in a day, whether you send three thousand requests through them or three million. If you are counting unique addresses and the number looks capped, divide it by your port count and you have found your rotation interval.
Your client has a granularity too, and it is the one nobody sets deliberately.
The rule: the addresses you observe are governed by whichever of the two is coarser. A per-connection endpoint behind a pooled client gives you one address. A per-request client against an interval endpoint gives you one address per interval. You get the slower dial, every time.
Check how your endpoint is documented to rotate, then check your client’s pooling behaviour. If they disagree, the coarser one wins, and it is usually not the one you were thinking about.
The customer from the opening asked his second question about a completely different product, and it was the right question both times. What changed was the answer, because the two endpoints rotate at different granularities — one hands out an address per connection, the other holds one address per port until its interval expires.
That is the thing worth carrying away. Before you conclude that a rotation is broken, find out what your endpoint rotates on, and then count that thing. On a per-connection endpoint it is your connections, and that is the one number in this picture you fully control. On an interval endpoint it is the clock and the number of ports, and no amount of connection churn will move it.
For what “unique” really means once you have the addresses, see the IP was never yours alone.