A proxy can hand you an IP in one of two ways. Rotating gives you a different address on every request. Sticky gives you the same address for as long as you need it. Pick the wrong one and you either hit a per-IP rate limit on one address or get flagged for teleporting between cities halfway through a login.
What each one is here
- Rotating: the residential endpoint,
resi.proxymonkey.io:8000. A different home IP on every request, billed by the gigabyte. - Static: an ISP or datacenter address you rent for a term. It stays yours until you release it, and you can swap it from the dashboard when you want a fresh one. Billed by the IP.
You can see the difference in three requests:
for i in 1 2 3; do
curl -s -x http://USER:[email protected]:8000 https://httpbin.org/ip
doneThree different origin values. Point the same loop at a static address and you get the same one three times.
When rotation helps
Rotation is for many independent requests with no state between them: search results, category listings, public product pages. Many rate limits are counted per IP, so spreading requests over many addresses spreads the limit too. You do not care which IP fetched which page, and neither does anything downstream.
When rotation hurts
Anything with a session. A login, a cart, a checkout, a search whose page two depends on a token from page one. The site hands you a cookie and expects it back from the same place. The same session cookie arriving from a new address on every request is one of the loudest signals there is; the blocking checklist covers it under cookies. Some sites also bind a form’s token to the IP that loaded the form, so the submit fails outright.
Pattern 1: rotating, with nothing linking the requests
import requests
PROXY = "http://USER:[email protected]:8000"
PROXIES = {"http": PROXY, "https": PROXY}
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"}
def fetch(url):
return requests.get(url, proxies=PROXIES, headers=HEADERS, timeout=25)
for url in ["https://httpbin.org/ip"] * 3:
print(fetch(url).json())Plain requests.get carries no cookie jar from one call to the next, which is what you want here. A requests.Session is different in two ways: it keeps cookies, and it reuses connections, so several HTTPS requests can travel through one tunnel. Run the httpbin loop above with a session before you rely on a new IP per request, and see what your own client does.
Pattern 2: one identity, one address, pinned
Put your static addresses in static.txt, one proxy URL per line in the http://USER:PASS@IP:PORT form, with the IP and port each address was assigned. Each identity gets one address and keeps it, including across restarts:
import json
import pathlib
import requests
ADDRESSES = [line.strip() for line in open("static.txt") if line.strip()]
IDENTITIES = ["store-a", "store-b", "store-c"]
PIN_FILE = pathlib.Path("pins.json")
pins = json.loads(PIN_FILE.read_text()) if PIN_FILE.exists() else {}
free = [a for a in ADDRESSES if a not in pins.values()]
for name in IDENTITIES:
if name not in pins:
if not free:
raise SystemExit("one address per identity: rent more, or run fewer")
pins[name] = free.pop(0)
PIN_FILE.write_text(json.dumps(pins, indent=2))
def session_for(name):
s = requests.Session()
s.proxies = {"http": pins[name], "https": pins[name]}
s.headers["User-Agent"] = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
)
return s
s = session_for("store-a")
print(s.get("https://httpbin.org/ip", timeout=25).json())The session keeps its cookies and its address together for the whole flow. pins.json holds your proxy credentials, so keep it out of git. If an identity is a login, check our acceptable use policy first: scraping behind a login you legitimately hold is a grey area we handle case by case, and we would rather you asked before starting.
Pattern 3: rotation you control, over a few static addresses
A common question: “I rented five datacenter IPs, how do I rotate them?” Round-robin, with a minimum gap per address so no single IP gets hammered:
import itertools
import time
MIN_GAP = 4.0 # seconds between two requests from the same address: your call
ADDRESSES = [line.strip() for line in open("static.txt") if line.strip()]
last_used = {a: 0.0 for a in ADDRESSES}
ring = itertools.cycle(ADDRESSES)
def next_proxies():
address = next(ring)
wait = last_used[address] + MIN_GAP - time.monotonic()
if wait > 0:
time.sleep(wait)
last_used[address] = time.monotonic()
return {"http": address, "https": address}Pass proxies=next_proxies() to each request. Your ceiling is the number of addresses divided by MIN_GAP requests per second. The gap is your guess at what the site tolerates, so start cautious and watch for 429s.
Scrapy: pair each proxy with its own cookie jar
Scrapy takes the proxy per request in meta, and the cookiejar key keeps separate cookie jars. Neither carries over to follow-up requests on its own, so pass both along:
import scrapy
PINS = {
"store-a": "http://USER:PASS@IP_1:PORT_1",
"store-b": "http://USER:PASS@IP_2:PORT_2",
}
class StoreSpider(scrapy.Spider):
name = "stores"
def start_requests(self):
for jar, (name, proxy) in enumerate(PINS.items()):
yield scrapy.Request(
f"https://example.com/{name}/",
meta={"proxy": proxy, "cookiejar": jar},
)
def parse(self, response):
next_page = response.css("a.next::attr(href)").get()
if next_page:
yield response.follow(
next_page,
meta={
"proxy": response.meta["proxy"],
"cookiejar": response.meta["cookiejar"],
},
)For the rotating case in Scrapy, set meta={"proxy": "http://USER:[email protected]:8000"} and leave out the cookie jar.
Node: one agent per address
import { readFileSync } from "node:fs";
import { fetch, ProxyAgent } from "undici";
const rotating = new ProxyAgent("http://USER:[email protected]:8000");
const addresses = readFileSync("static.txt", "utf8").split("\n").filter(Boolean);
const pinned = new Map(
["store-a", "store-b"].map((name, i) => [name, new ProxyAgent(addresses[i])]),
);
const a = await fetch("https://httpbin.org/ip", { dispatcher: rotating });
const b = await fetch("https://httpbin.org/ip", { dispatcher: pinned.get("store-a") });
console.log(await a.json(), await b.json());Importing fetch from undici along with ProxyAgent keeps both from the same package version, which avoids a class of confusing errors when Node’s built-in fetch and the installed undici disagree.
What each costs
Rotating has no idle cost: you pay for the bytes you move, from $5.50/GB for a single gigabyte down to $1.75/GB at volume. Static costs the same whether you use it or not: for 30 days, a shared datacenter address is $2.10, a dedicated one $3.20, and a dedicated ISP address $3.20. Static addresses are bought for a country you pick at checkout; the residential pool does not take a country.
Plenty of jobs use both: rotate through the listing pages, then pin one static address to each flow that needs a session. If you are not sure which side of that line your target falls on, the flowchart is four questions long.
Top-ups start at $5.
One shared datacenter IP for 30 days is $2.10. A single gigabyte of residential is $5.50. The balance never expires.