[Examples & benchmarks](https://cdpfleet.com/docs/cases) / #25 · 2026-10-07 · Example · Easy · for [geo testing](https://cdpfleet.com/geo-testing), [web scraping](https://cdpfleet.com/web-scraping)

# 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.

[Chromium](https://cdpfleet.com/docs/browsers/chromium)

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](https://cdpfleet.com/docs/proxies#residential) 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

1.  Launch Chromium four times with four tokens: `cdpfleet-resi`, `cdpfleet-resi-country-de`, and twice `cdpfleet-resi-country-us-session-<name>-lifetime-10`.
2.  In each browser, look up the exit IP and country three times.
3.  Compare: how many distinct IPs each browser saw, which countries, and whether the second sticky browser got the first one's IP.
4.  Read the balance and price from the Account API.

## The code

The same program in five languages (also [on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/25-residential-without-a-proxy), with the raw output). Set these environment variables first:

-   `CDPFLEET_API_KEY` — your API key (dashboard → API keys)

 

```node
// npm install playwright@1.60.0
// 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));
```

```python
# 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))
```

```java
// 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));
  }
}
```

```csharp
// 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
// go get github.com/playwright-community/playwright-go@v0.6000.0
// 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](https://cdpfleet.com/docs/cases/bandwidth-diet) techniques or `proxy_rules` to 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`.

---
Source: https://cdpfleet.com/docs/cases/residential-without-a-proxy · Markdown: https://cdpfleet.com/docs/cases/residential-without-a-proxy.md · All docs for AI tools: https://cdpfleet.com/llms.txt
