Cases / #25 · 2026-10-07 · Easy
No proxy of your own: rotating, country and sticky residential IPs with one token
Four launches on the cdpfleet residential proxy — any country, Germany, and one sticky US identity carried from one browser session to the next — with nothing but a token as the proxy.
Run on production on 2026-10-07: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go
The problem
Every cdpfleet session needs a proxy, and until now that meant buying one somewhere else first. Residential IPs are what most scraping and account work needs: real ISP addresses in the right country, a new one per request for crawling, or the same one for a whole login flow. The cdpfleet residential proxy puts all of that in the proxy field — cdpfleet-resi, plus country, device OS and a sticky session name — billed from a prepaid balance. Does the targeting hold, does a sticky IP really survive into the next browser, and what does it look like from the site's side?
What we used, and why
| What | Why |
|---|---|
"proxy": "cdpfleet-resi" | A new residential IP for every connection, any country. No credentials anywhere in your code: the upstream is configured on our servers. |
-country-de | Exit in Germany; any ISO two-letter code works. |
-session-<name>-lifetime-10 | A sticky session: every connection with the same name uses the same IP for up to 10 minutes. Names are private to your account. |
| Two browser sessions with the same sticky name | Browser 2 starts after browser 1 has closed — the question is whether the identity carries over. |
http://ip-api.com/json three times per browser | Three separate connections: rotating exits should change, sticky ones shouldn't. Each lookup is retried — residential peers drop a few percent of connections. |
GET /v1/me/proxy-balance | The prepaid balance the traffic is billed to ($5/GB, 1 GB free on every account). |
How it works
- Launch Chromium four times with four tokens:
cdpfleet-resi,cdpfleet-resi-country-de, and twicecdpfleet-resi-country-us-session-<name>-lifetime-10. - In each browser, look up the exit IP and country three times.
- Compare: how many distinct IPs each browser saw, which countries, and whether the second sticky browser got the first one's IP.
- Read the balance and price from the Account API.
The code
The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:
CDPFLEET_API_KEY— your API key (dashboard → API keys)
// npm install [email protected]
// env: CDPFLEET_API_KEY (no proxy of your own needed)
import { chromium } from 'playwright';
const KEY = process.env.CDPFLEET_API_KEY;
const SESSION = `c25${Date.now().toString(36).slice(-6)}`; // a sticky name, 1–10 of [A-Za-z0-9_]
// Four launches, all on the cdpfleet residential proxy — the token is the whole proxy config.
const RUNS = [
{ label: 'rotating, any country', proxy: 'cdpfleet-resi' },
{ label: 'rotating, Germany', proxy: 'cdpfleet-resi-country-de' },
{ label: 'sticky US, browser 1', proxy: `cdpfleet-resi-country-us-session-${SESSION}-lifetime-10` },
{ label: 'sticky US, browser 2', proxy: `cdpfleet-resi-country-us-session-${SESSION}-lifetime-10` },
];
// Residential peers drop a few percent of connections: retry a lookup up to 3 times.
async function lookup(page, i) {
for (let attempt = 1; ; attempt++) {
try {
return await (await page.goto(`http://ip-api.com/json/?fields=query,countryCode&i=${i}-${attempt}`, { timeout: 60000 })).json();
} catch (err) {
if (attempt === 3) throw err;
}
}
}
async function run({ label, proxy }) {
const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
method: 'POST',
headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
body: JSON.stringify({ proxy, headless: 'new' }),
});
if (!res.ok) return { label, error: `launch ${res.status} ${await res.text()}` };
const { wsUrl } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });
try {
const page = await browser.newPage();
// Three lookups on separate connections: rotating exits change, sticky ones don't.
const exits = [];
for (let i = 0; i < 3; i++) exits.push(await lookup(page, i));
return {
label,
proxy: proxy.replace(SESSION, '<name>'),
countries: [...new Set(exits.map((e) => e.countryCode))].join(','),
distinct_ips: new Set(exits.map((e) => e.query)).size,
first_ip: exits[0].query,
};
} finally {
await browser.close();
}
}
const out = [];
for (const r of RUNS) out.push(await run(r)); // in order, so browser 2 starts after browser 1 ended
const [b1, b2] = out.slice(2);
for (const r of out) r.same_ip_as_browser_1 = r.label.startsWith('sticky') ? r.first_ip === b1.first_ip : null;
// The balance the traffic is billed to (metered about once a minute, so it trails a little).
const bal = await (await fetch('https://cdpfleet.com/v1/me/proxy-balance', { headers: { 'x-api-key': KEY } })).json();
for (const r of out) delete r.first_ip;
console.log(JSON.stringify({ runs: out, sticky_ip_kept_across_browsers: b2.same_ip_as_browser_1, balance_allowed: bal.allowed, price_per_gb_usd: bal.price_per_gb_usd }, null, 2));
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY (no proxy of your own needed)
import json
import os
import time
import requests
from playwright.sync_api import sync_playwright
KEY = os.environ["CDPFLEET_API_KEY"]
def base36(n):
digits = "0123456789abcdefghijklmnopqrstuvwxyz"
out = ""
while n:
n, r = divmod(n, 36)
out = digits[r] + out
return out or "0"
SESSION = "c25" + base36(int(time.time() * 1000))[-6:] # a sticky name, 1–10 of [A-Za-z0-9_]
# Four launches, all on the cdpfleet residential proxy — the token is the whole proxy config.
RUNS = [
{"label": "rotating, any country", "proxy": "cdpfleet-resi"},
{"label": "rotating, Germany", "proxy": "cdpfleet-resi-country-de"},
{"label": "sticky US, browser 1", "proxy": f"cdpfleet-resi-country-us-session-{SESSION}-lifetime-10"},
{"label": "sticky US, browser 2", "proxy": f"cdpfleet-resi-country-us-session-{SESSION}-lifetime-10"},
]
def lookup(page, i):
# Residential peers drop a few percent of connections: retry a lookup up to 3 times.
for attempt in range(1, 4):
try:
return page.goto(f"http://ip-api.com/json/?fields=query,countryCode&i={i}-{attempt}", timeout=60000).json()
except Exception:
if attempt == 3:
raise
def run(p, label, proxy):
res = requests.post("https://starter.cdpfleet.com/chromium/session", headers={"x-api-key": KEY}, timeout=60,
json={"proxy": proxy, "headless": "new"})
if not res.ok:
return {"label": label, "error": f"launch {res.status_code} {res.text}"}
browser = p.chromium.connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
try:
page = browser.new_page()
# Three lookups on separate connections: rotating exits change, sticky ones don't.
exits = [lookup(page, i) for i in range(3)]
countries = []
for e in exits:
if e["countryCode"] not in countries:
countries.append(e["countryCode"])
return {
"label": label,
"proxy": proxy.replace(SESSION, "<name>"),
"countries": ",".join(countries),
"distinct_ips": len({e["query"] for e in exits}),
"first_ip": exits[0]["query"],
}
finally:
browser.close()
with sync_playwright() as p:
out = [run(p, r["label"], r["proxy"]) for r in RUNS] # in order, so browser 2 starts after browser 1 ended
b1, b2 = out[2], out[3]
for r in out:
r["same_ip_as_browser_1"] = r["first_ip"] == b1["first_ip"] if r["label"].startswith("sticky") else None
# The balance the traffic is billed to (metered about once a minute, so it trails a little).
bal = requests.get("https://cdpfleet.com/v1/me/proxy-balance", headers={"x-api-key": KEY}, timeout=30).json()
for r in out:
del r["first_ip"]
print(json.dumps({
"runs": out,
"sticky_ip_kept_across_browsers": b2["same_ip_as_browser_1"],
"balance_allowed": bal["allowed"],
"price_per_gb_usd": bal["price_per_gb_usd"],
}, indent=2))
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1. env: CDPFLEET_API_KEY (no proxy of your own needed)
import com.google.gson.*;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.util.*;
public class Main {
static final String KEY = System.getenv("CDPFLEET_API_KEY");
static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().serializeNulls().create();
static final HttpClient HTTP = HttpClient.newHttpClient();
static final String B36 = Long.toString(System.currentTimeMillis(), 36);
static final String SESSION = "c25" + B36.substring(B36.length() - 6); // a sticky name, 1–10 of [A-Za-z0-9_]
// Four launches, all on the cdpfleet residential proxy — the token is the whole proxy config.
static final String[][] RUNS = {
{"rotating, any country", "cdpfleet-resi"},
{"rotating, Germany", "cdpfleet-resi-country-de"},
{"sticky US, browser 1", "cdpfleet-resi-country-us-session-" + SESSION + "-lifetime-10"},
{"sticky US, browser 2", "cdpfleet-resi-country-us-session-" + SESSION + "-lifetime-10"},
};
// Residential peers drop a few percent of connections: retry a lookup up to 3 times.
static JsonObject lookup(Page page, int i) {
for (int attempt = 1; ; attempt++) {
try {
Response r = page.navigate("http://ip-api.com/json/?fields=query,countryCode&i=" + i + "-" + attempt,
new Page.NavigateOptions().setTimeout(60000));
return JsonParser.parseString(r.text()).getAsJsonObject();
} catch (RuntimeException e) {
if (attempt == 3) throw e;
}
}
}
static JsonObject run(Playwright playwright, String label, String proxy) throws Exception {
JsonObject body = new JsonObject();
body.addProperty("proxy", proxy);
body.addProperty("headless", "new");
HttpResponse<String> res = HTTP.send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
.header("x-api-key", KEY).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body.toString())).build(), HttpResponse.BodyHandlers.ofString());
JsonObject row = new JsonObject();
row.addProperty("label", label);
if (res.statusCode() != 200) {
row.addProperty("error", "launch " + res.statusCode() + " " + res.body());
return row;
}
String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
Browser browser = playwright.chromium().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
try {
Page page = browser.newPage();
// Three lookups on separate connections: rotating exits change, sticky ones don't.
List<JsonObject> exits = new ArrayList<>();
for (int i = 0; i < 3; i++) exits.add(lookup(page, i));
LinkedHashSet<String> countries = new LinkedHashSet<>();
Set<String> ips = new HashSet<>();
for (JsonObject e : exits) {
countries.add(e.get("countryCode").getAsString());
ips.add(e.get("query").getAsString());
}
row.addProperty("proxy", proxy.replace(SESSION, "<name>"));
row.addProperty("countries", String.join(",", countries));
row.addProperty("distinct_ips", ips.size());
row.addProperty("first_ip", exits.get(0).get("query").getAsString());
return row;
} finally {
browser.close();
}
}
public static void main(String[] args) throws Exception {
List<JsonObject> out = new ArrayList<>();
try (Playwright playwright = Playwright.create()) {
for (String[] r : RUNS) out.add(run(playwright, r[0], r[1])); // in order, so browser 2 starts after browser 1 ended
}
JsonObject b1 = out.get(2);
JsonObject b2 = out.get(3);
for (JsonObject r : out) {
if (r.get("label").getAsString().startsWith("sticky")) {
r.addProperty("same_ip_as_browser_1", r.get("first_ip").getAsString().equals(b1.get("first_ip").getAsString()));
} else {
r.add("same_ip_as_browser_1", JsonNull.INSTANCE);
}
}
// The balance the traffic is billed to (metered about once a minute, so it trails a little).
HttpResponse<String> balRes = HTTP.send(HttpRequest.newBuilder(URI.create("https://cdpfleet.com/v1/me/proxy-balance"))
.header("x-api-key", KEY).GET().build(), HttpResponse.BodyHandlers.ofString());
JsonObject bal = JsonParser.parseString(balRes.body()).getAsJsonObject();
JsonArray runs = new JsonArray();
for (JsonObject r : out) {
r.remove("first_ip");
runs.add(r);
}
JsonObject doc = new JsonObject();
doc.add("runs", runs);
doc.add("sticky_ip_kept_across_browsers", b2.get("same_ip_as_browser_1"));
doc.add("balance_allowed", bal.get("allowed"));
doc.add("price_per_gb_usd", bal.get("price_per_gb_usd"));
System.out.println(GSON.toJson(doc));
}
}
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY (no proxy of your own needed)
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;
var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
static string Base36(long n)
{
const string digits = "0123456789abcdefghijklmnopqrstuvwxyz";
var s = "";
while (n > 0) { s = digits[(int)(n % 36)] + s; n /= 36; }
return s;
}
var b36 = Base36(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
var session = "c25" + b36[^6..]; // a sticky name, 1–10 of [A-Za-z0-9_]
// Four launches, all on the cdpfleet residential proxy — the token is the whole proxy config.
var runs = new[]
{
(Label: "rotating, any country", Proxy: "cdpfleet-resi"),
(Label: "rotating, Germany", Proxy: "cdpfleet-resi-country-de"),
(Label: "sticky US, browser 1", Proxy: $"cdpfleet-resi-country-us-session-{session}-lifetime-10"),
(Label: "sticky US, browser 2", Proxy: $"cdpfleet-resi-country-us-session-{session}-lifetime-10"),
};
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
using var playwright = await Playwright.CreateAsync();
// Residential peers drop a few percent of connections: retry a lookup up to 3 times.
async Task<JsonElement> Lookup(IPage page, int i)
{
for (var attempt = 1; ; attempt++)
{
try
{
var r = await page.GotoAsync($"http://ip-api.com/json/?fields=query,countryCode&i={i}-{attempt}", new() { Timeout = 60000 });
return JsonDocument.Parse(await r!.TextAsync()).RootElement.Clone();
}
catch (Exception e) when (e is PlaywrightException or TimeoutException or JsonException)
{
if (attempt == 3) throw;
}
}
}
async Task<(JsonObject Row, string? FirstIp)> Run(string label, string proxy)
{
var row = new JsonObject { ["label"] = label };
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chromium/session", new { proxy, headless = "new" });
if (!res.IsSuccessStatusCode)
{
row["error"] = $"launch {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}";
return (row, null);
}
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;
var browser = await playwright.Chromium.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
try
{
var page = await browser.NewPageAsync();
// Three lookups on separate connections: rotating exits change, sticky ones don't.
var exits = new List<JsonElement>();
for (var i = 0; i < 3; i++) exits.Add(await Lookup(page, i));
row["proxy"] = proxy.Replace(session, "<name>");
row["countries"] = string.Join(",", exits.Select(e => e.GetProperty("countryCode").GetString()).Distinct());
row["distinct_ips"] = exits.Select(e => e.GetProperty("query").GetString()).Distinct().Count();
return (row, exits[0].GetProperty("query").GetString());
}
finally
{
await browser.CloseAsync();
}
}
var results = new List<(JsonObject Row, string? FirstIp)>();
foreach (var r in runs) results.Add(await Run(r.Label, r.Proxy)); // in order, so browser 2 starts after browser 1 ended
var b1Ip = results[2].FirstIp;
foreach (var (row, ip) in results)
row["same_ip_as_browser_1"] = row["label"]!.GetValue<string>().StartsWith("sticky") ? JsonValue.Create(ip == b1Ip) : null;
// The balance the traffic is billed to (metered about once a minute, so it trails a little).
var bal = await http.GetFromJsonAsync<JsonElement>("https://cdpfleet.com/v1/me/proxy-balance");
var doc = new JsonObject
{
["runs"] = new JsonArray(results.Select(x => (JsonNode)x.Row).ToArray()),
["sticky_ip_kept_across_browsers"] = results[3].Row["same_ip_as_browser_1"]?.DeepClone(),
["balance_allowed"] = bal.GetProperty("allowed").GetBoolean(),
["price_per_gb_usd"] = JsonNode.Parse(bal.GetProperty("price_per_gb_usd").GetRawText()),
};
Console.WriteLine(doc.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
// go get github.com/playwright-community/[email protected]
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY (no proxy of your own needed)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/playwright-community/playwright-go"
)
var key = os.Getenv("CDPFLEET_API_KEY")
type row struct {
Label string `json:"label"`
Proxy string `json:"proxy,omitempty"`
Countries string `json:"countries,omitempty"`
DistinctIPs int `json:"distinct_ips,omitempty"`
SameIPAsBrowser1 *bool `json:"same_ip_as_browser_1"`
Error string `json:"error,omitempty"`
firstIP string
}
type exit struct {
Query string `json:"query"`
CountryCode string `json:"countryCode"`
}
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
// Residential peers drop a few percent of connections: retry a lookup up to 3 times.
func lookup(page playwright.Page, i int) exit {
for attempt := 1; ; attempt++ {
r, err := page.Goto(fmt.Sprintf("http://ip-api.com/json/?fields=query,countryCode&i=%d-%d", i, attempt),
playwright.PageGotoOptions{Timeout: playwright.Float(60000)})
var e exit
if err == nil {
if body, berr := r.Body(); berr == nil {
if jerr := json.Unmarshal(body, &e); jerr == nil {
return e
} else {
err = jerr
}
} else {
err = berr
}
}
if attempt == 3 {
log.Fatal(err)
}
}
}
func run(pw *playwright.Playwright, label, proxy, session string) row {
body, _ := json.Marshal(map[string]any{"proxy": proxy, "headless": "new"})
req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/chromium/session", bytes.NewReader(body))
req.Header.Set("x-api-key", key)
req.Header.Set("content-type", "application/json")
res := must(http.DefaultClient.Do(req))
defer res.Body.Close()
raw, _ := io.ReadAll(res.Body)
if res.StatusCode != http.StatusOK {
return row{Label: label, Error: fmt.Sprintf("launch %d %s", res.StatusCode, raw)}
}
var s struct {
WsURL string `json:"wsUrl"`
}
must(0, json.Unmarshal(raw, &s))
browser := must(pw.Chromium.Connect(s.WsURL, playwright.BrowserTypeConnectOptions{
Headers: map[string]string{"x-api-key": key},
}))
defer browser.Close()
page := must(browser.NewPage())
// Three lookups on separate connections: rotating exits change, sticky ones don't.
var exits []exit
for i := 0; i < 3; i++ {
exits = append(exits, lookup(page, i))
}
var countries []string
seenCountry := map[string]bool{}
ips := map[string]bool{}
for _, e := range exits {
if !seenCountry[e.CountryCode] {
seenCountry[e.CountryCode] = true
countries = append(countries, e.CountryCode)
}
ips[e.Query] = true
}
return row{
Label: label,
Proxy: strings.ReplaceAll(proxy, session, "<name>"),
Countries: strings.Join(countries, ","),
DistinctIPs: len(ips),
firstIP: exits[0].Query,
}
}
func main() {
b36 := strconv.FormatInt(time.Now().UnixMilli(), 36)
session := "c25" + b36[len(b36)-6:] // a sticky name, 1–10 of [A-Za-z0-9_]
// Four launches, all on the cdpfleet residential proxy — the token is the whole proxy config.
runs := [][2]string{
{"rotating, any country", "cdpfleet-resi"},
{"rotating, Germany", "cdpfleet-resi-country-de"},
{"sticky US, browser 1", "cdpfleet-resi-country-us-session-" + session + "-lifetime-10"},
{"sticky US, browser 2", "cdpfleet-resi-country-us-session-" + session + "-lifetime-10"},
}
pw := must(playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true}))
defer pw.Stop()
var out []row
for _, r := range runs { // in order, so browser 2 starts after browser 1 ended
out = append(out, run(pw, r[0], r[1], session))
}
b1 := out[2]
for i := range out {
if strings.HasPrefix(out[i].Label, "sticky") {
same := out[i].firstIP == b1.firstIP
out[i].SameIPAsBrowser1 = &same
}
}
// The balance the traffic is billed to (metered about once a minute, so it trails a little).
req, _ := http.NewRequest("GET", "https://cdpfleet.com/v1/me/proxy-balance", nil)
req.Header.Set("x-api-key", key)
res := must(http.DefaultClient.Do(req))
defer res.Body.Close()
var bal struct {
Allowed bool `json:"allowed"`
PricePerGbUsd float64 `json:"price_per_gb_usd"`
}
must(0, json.NewDecoder(res.Body).Decode(&bal))
doc := struct {
Runs []row `json:"runs"`
StickyIPKeptAcrossBrowsers *bool `json:"sticky_ip_kept_across_browsers"`
BalanceAllowed bool `json:"balance_allowed"`
PricePerGbUsd float64 `json:"price_per_gb_usd"`
}{out, out[3].SameIPAsBrowser1, bal.Allowed, bal.PricePerGbUsd}
enc := json.NewEncoder(os.Stdout)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
must(0, enc.Encode(doc))
}
What we got
| Launch | proxy | Exit countries | IPs in 3 lookups | Same IP as browser 1 |
|---|---|---|---|---|
| rotating, any country | cdpfleet-resi | IT,FR,IN | 3 | — |
| rotating, Germany | cdpfleet-resi-country-de | DE | 3 | — |
| sticky US, browser 1 | cdpfleet-resi-country-us-session-<name>-lifetime-10 | US | 1 | yes |
| sticky US, browser 2 | cdpfleet-resi-country-us-session-<name>-lifetime-10 | US | 1 | yes |
From the Node.js run on 2026-10-07. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.
Raw output (Node.js)
{
"runs": [
{
"label": "rotating, any country",
"proxy": "cdpfleet-resi",
"countries": "IT,FR,IN",
"distinct_ips": 3,
"same_ip_as_browser_1": null
},
{
"label": "rotating, Germany",
"proxy": "cdpfleet-resi-country-de",
"countries": "DE",
"distinct_ips": 3,
"same_ip_as_browser_1": null
},
{
"label": "sticky US, browser 1",
"proxy": "cdpfleet-resi-country-us-session-<name>-lifetime-10",
"countries": "US",
"distinct_ips": 1,
"same_ip_as_browser_1": true
},
{
"label": "sticky US, browser 2",
"proxy": "cdpfleet-resi-country-us-session-<name>-lifetime-10",
"countries": "US",
"distinct_ips": 1,
"same_ip_as_browser_1": true
}
],
"sticky_ip_kept_across_browsers": true,
"balance_allowed": true,
"price_per_gb_usd": 5
}Takeaways
- The token is the whole proxy setup: no account with a proxy vendor, no credentials in code, nothing to rotate — and the same token works in all five languages and in
proxy_rules. - Rotating means rotating: three lookups gave three different IPs, in a different country each time when no country was set, and in Germany with
-country-de— in all but one of our runs, where a geo-IP database placed one German exit elsewhere. Databases disagree about residential ranges; check the country with the service the site itself is likely to use. - Sticky means sticky, across browsers: both US sessions saw one IP each — and the same IP as each other. Reuse the name to keep an identity through a login in one browser and the work in the next.
- Billing is by traffic, not by the token: a lookup is a few kilobytes; a full page with images is hundreds. Use bandwidth diet techniques or
proxy_rulesto send only the requests that need a residential IP through it. - Plan for failures: residential connections belong to real people's devices and a few percent fail — retry a request, and retry a launch that returns
503.