How to Rotate Proxy IPs: Three Mechanisms Compared
Rotation can happen at the gateway, in your own client code, or as a one-off refresh. How each works, how to set a rate, and what none of them fix.
"Rotate the IP" can mean three different things, and they belong to different products:
| Mechanism | Who rotates | What you configure | Product |
|---|---|---|---|
| Gateway rotation | Node4, on every new connection | One hostname and your credential | Rotating and residential plans |
| Client-side cycling | Your own code, across a list of static addresses | A scheduler you write | Shared or dedicated datacenter plans |
| Refresh IP | You, once, to replace one address that has gone bad | A button or an API call | Paid static plans |
You do not need a rotating plan to rotate, and buying one does not remove the need to think about rate. Rotating versus static proxies works through which shape suits which job; this guide covers the mechanics of each.
Gateway rotation: no rotation code
Point your client at the gateway hostname with your credential from the dashboard, and the pool management happens behind it:
curl -x http://USERNAME:PASSWORD@gw-rotating_datacenter.node4.io:8083 https://api.ipify.org
curl -x http://USERNAME:PASSWORD@gw-rotating_datacenter.node4.io:8083 https://api.ipify.orgRun it twice and the two addresses will generally differ: each curl run opens its own HTTPS tunnel, and each tunnel is assigned an exit independently.
Per connection, not per request
The gateway chooses the exit once per proxied connection. What that means depends on the traffic:
- Plain HTTP (
http://targets): the gateway picks an exit for every request, even when your client keeps its connection to the gateway open. - HTTPS (
https://targets) and SOCKS5: the exit is chosen when theCONNECTtunnel or SOCKS5 connection opens, and every request sent down it leaves from the same exit. A client with keep-alive enabled, which is every modern HTTP client by default, reuses the tunnel across many requests.
If an HTTPS run rotates far less than you expected, you are reusing connections. To force a fresh exit per request, disable reuse: a single curl command that fetches several URLs reuses one tunnel, so run one URL per command; in a library, turn off keep-alive or the connection pool for the proxied client. The reverse is also useful: a few requests deliberately sharing one connection is the cheapest way to keep a short interaction on a single exit. To hold an exit for longer than one connection, use a sticky session, covered in sticky vs rotating sessions.
Rotating within one country
Rotation can be narrowed to a location with a segment in the username, and the gateway still picks a fresh exit per connection inside it:
curl -v -x http://USERNAME-country-us:PASSWORD@gw-rotating_datacenter.node4.io:8083 https://api.ipify.orgOn Rotating Datacenter and Rotating Unmetered the segment is -country-xx and nothing finer; rotating exits are in the United States, Italy and Spain. A country with no exits is refused with a 502 rather than served from somewhere else. Refusals and plain-HTTP responses carry an X-Node4-Country header (honored, unavailable or none; curl -v prints it); a successful HTTPS tunnel does not, because the gateway answers the CONNECT before the destination does, and SOCKS5 carries no headers at all. none means no country segment was parsed. A misspelled key such as -contry- does not fall back to worldwide: the whole username is unrecognized and the gateway answers 407.
The residential gateway goes further: segments choose a country, a state, a city or a network operator. That syntax is in country and city targeting. To get these strings from code instead of assembling them by hand, building proxy strings with the gateway API returns them by country, city or sticky session.
Client-side cycling: rotation you own
A static plan gives you addresses that do not change, and your own code decides which request goes out through which one. The Proxies page lists every address, and the management API serves the same list as JSON from /proxies/list; code that fetches the list at startup picks up any address you later replace.
A minimal round-robin scheduler. It reads the address list from the API with a key that has the read:proxies scope, and authenticates every address with your one account credential from the Proxies page:
import itertools
import os
from urllib.parse import quote
import requests
API = "https://node4.io/api/v1"
KEY = os.environ["NODE4_API_KEY"]
USER = quote(os.environ["NODE4_PROXY_USER"], safe="")
PASS = quote(os.environ["NODE4_PROXY_PASS"], safe="")
resp = requests.get(
f"{API}/proxies/list",
headers={"Authorization": f"Bearer {KEY}"},
timeout=30,
)
resp.raise_for_status()
rows = resp.json()["data"] # active proxies: host, port, type, country, ...
pool = itertools.cycle(rows)
def fetch(url):
p = next(pool)
proxy = f"http://{USER}:{PASS}@{p['host']}:{p['port']}"
return requests.get(url, proxies={"http": proxy, "https": proxy}, timeout=30)
for _ in range(3):
print(fetch("https://api.ipify.org").text)Each call takes the next address in the list, so with three or more proxies the loop prints three different addresses.
Round-robin is predictable: every address carries an equal share, and a per-address problem shows up in your logs as a periodic pattern. The refinement worth building is error-driven rest: when one address starts drawing 403s or CAPTCHAs from a target, take it out of rotation for that target for an hour. Why is my IP getting blocked describes what those early warnings look like.
Against a gateway you give up variety, since you cycle only through the addresses your plan includes, and gain control: you can pin one address to one logged-in account for its whole life while cycling the rest, which a per-connection gateway cannot express. How many proxies do I need sizes the static side.
Refresh IP: a replacement, not a rotation
On paid static plans, each proxy row on the Proxies page has a Refresh IP action, also available through the API as POST /proxies/{id}/replace or POST /proxies/refresh (a key with the write:proxies scope). It retires that address and issues a different one. It is not available on the free tier.
Refreshes come from a monthly allowance set by your plan, counted across the whole account and reset on the first of each calendar month; a plan whose allowance is zero includes none. The confirmation dialog on the Proxies page shows how many you have used this month out of your limit, and support can raise the limit for an account that needs more. Once it is spent, the dashboard refuses further refreshes until the month turns, and POST /proxies/{id}/replace answers 429 with a resetsAt time and a Retry-After header. Automating IP replacement and proxy health shows a script that replaces only the addresses failing a health check, which keeps the allowance for the ones that need it.
Use it for an address that has accumulated a bad history at the one target you bought it for. After a refresh the old address is gone, so anything that hard-codes it (a config file, a firewall rule, a partner's allow list) needs updating, which is why reading /proxies/list at startup beats pasting addresses into source.
If you find yourself refreshing every few days as a target wears each address down, the workload wants gateway rotation. The two are compared directly, with the API call and the connection string side by side, in proxy rotation API or rotating gateway.
How often to rotate
Often enough that no single exit crosses the target's per-address threshold inside its window. That is a number you measure: send from a single address at a modest, steady pace and watch for the first soft resistance (response times stretching, intermittent 429s, a CAPTCHA where a page used to be). Note the requests per hour you had reached, take a margin off, and call that your per-address budget for this target. Your required throughput divided by that budget is the number of exits you need in play at once.
Rotating slower than the budget requires burns addresses. Rotating much faster is not a free safety margin: it churns exits you had no need to churn, and per-request rotation under a client that keeps cookies presents the target with one visitor arriving from hundreds of networks in an hour, which is itself a strong signal. For client-side pools, combine a schedule sized from the measured budget with an immediate rotation on a 403, 429 or CAPTCHA, resting the address that drew it.
What rotation does not change
Rotation changes the network address and nothing else. Every other identifier travels to the new exit unchanged:
- Cookies. A cookie jar that persists across a rotation lets the target stitch the visitors back together. Clear it when the exit changes, or scope one jar to each exit.
- Logins. An authenticated account is a stronger identity than any address. Rotating under a logged-in session tends to look like a hijacked account and trigger a forced logout. Work bound to accounts wants one stable exit per account.
- Browser and TLS fingerprints. A headless browser presents the same fingerprint from every exit.
- Address classification. Cycling within a datacenter pool changes which datacenter address you use, not that it is one. Moving that ceiling means moving pools, which is the case for residential exits.
One failure is caused by rotation rather than cured by it: a flow where the server holds state against your visit (a login, a cart, a paginated result set) breaks when the exit changes mid-flow. If your symptom is a logout or a vanished cart, sticky vs rotating sessions is the fix.
Where to start
For stateless fetching at volume, start with a gateway plan (compared on the pricing page) and spend your effort on rate measurement rather than pool plumbing. For a modest set of targets that need stable, controllable identities, start with static datacenter addresses and the scheduler above. The free tier (3 free shared proxies with 1 GB/month bandwidth and 10 concurrent threads) is enough to prototype the client-side scheduler against a real endpoint.
To see per-connection rotation for yourself, rotate_per_request.py in our examples repository on GitHub compares a reused session with a new connection per request.