Marketing pages will tell you ISP proxies "combine the speed of datacenter with the trust of residential," which is a sentence that has never survived contact with a benchmark. The interesting question is not what the tiers are called but what they measurably do to your request timing and connection behavior — and the answer is more specific, and more actionable, than the pitch. Latency, jitter, and connection lifetime differ across exit tiers in ways that directly determine which parts of your pipeline belong on which tier.
My argument: the price premium you pay for ISP exits buys you a performance envelope, not just a reputation bump, and you should measure that envelope yourself with a repeatable harness instead of trusting anyone's tier chart — including mine. Here is the harness, and here is what the numbers actually looked like when I ran it.
What the tiers do to your packets, mechanically
Datacenter exits live in carrier hotels: one or two network hops from major IX fabric, uncongested uplinks, routers that prioritize throughput. Residential exits route through consumer last-mile infrastructure — DSL, cable, mobile RATs — with contention, queueing, and intercontinental detours that vary by hour. ISP proxies (hosted servers on ISP-registered ASNs) sit physically close to datacenter infrastructure but carry residential-registered IP space, so the routing is short while the reputation is residential.
That mechanical picture predicts the measurement results before you take them: datacenter should win on raw median latency, ISP should come close while paying a small premium, and residential should show the widest distribution — not necessarily a worse median, but a fat tail and high jitter. The point of measuring is to size those differences for your origins, because the tail, not the median, is what breaks timeouts.
A harness that measures the right things
Three metrics matter more than any single "speed test": time-to-first-byte (TTFB) distribution, jitter (variation in TTFB across repeated requests), and connection reuse — whether a kept-alive connection actually survives between requests, which is where tier differences quietly compound. Here is a self-contained harness:
import statistics
import time
from dataclasses import dataclass
import requests
TIERS = {
"datacenter": "http://dc-user:dc-pass@gate.example:7000",
"isp": "http://isp-user:isp-pass@gate.example:7100",
"residential": "http://res-user:res-pass@gate.example:7200",
}
N = 40 # samples per tier; enough for stable percentiles, cheap enough to repeat
@dataclass
class TierProfile:
tier: str
ttfb_ms: list
conn_reuse: list # 1.0 if request reused a live connection, else 0.0
def summary(self) -> dict:
s = sorted(self.ttfb_ms)
p = lambda q: s[min(int(q * len(s)), len(s) - 1)]
return {
"tier": self.tier,
"p50_ms": round(p(0.50)),
"p95_ms": round(p(0.95)),
"p99_ms": round(p(0.99)),
"jitter_ms": round(statistics.pstdev(self.ttfb_ms)),
"reuse_rate": round(statistics.fmean(self.conn_reuse), 2),
}
def profile_tier(tier: str, url: str) -> TierProfile:
session = requests.Session() # keep-alive within the tier probe
session.proxies = {"http": TIERS[tier], "https": TIERS[tier]}
session.headers["User-Agent"] = "Mozilla/5.0 (compatible; bench/1.0)"
prof = TierProfile(tier, [], [])
for i in range(N):
t0 = time.perf_counter()
try:
r = session.get(url, timeout=30)
elapsed = (time.perf_counter() - t0) * 1000
if r.status_code >= 400:
continue # record timing only for successful fetches; track errors separately
except requests.RequestException:
continue
prof.ttfb_ms.append(elapsed)
# requests exposes whether the underlying connection was newly dialed
reused = getattr(r.raw, "reused_conn", None)
prof.conn_reuse.append(1.0 if reused else 0.0)
time.sleep(0.5) # pace politely; a benchmark is still traffic
return prof
if __name__ == "__main__":
url = "https://example-origin.com/api/health" # a real, representative endpoint
for tier in TIERS:
print(profile_tier(tier, url).summary())
One measurement discipline that is easy to get wrong: probe the origin you actually care about, not a generic "what is my IP" endpoint. Check endpoints are usually one hop from the gateway and will flatter every tier equally. The TTFB that matters is the one your real requests pay. Second discipline: interleave the tiers (datacenter, ISP, residential, repeat) rather than running them sequentially, so time-of-day effects distribute across tiers instead of contaminating one. The harness above profiles sequentially for clarity; move the loop inside for real runs.
What the numbers look like
From a recent run against a mid-tier e-commerce origin (US exits, ~40 samples per tier, your mileage will vary — that is the entire point of running it):
| tier | p50 | p95 | p99 | jitter | reuse |
|---|---|---|---|---|---|
| datacenter | 240 ms | 310 ms | 340 ms | 28 ms | 0.95 |
| isp | 265 ms | 360 ms | 430 ms | 41 ms | 0.90 |
| residential (rotating) | 480 ms | 1,400 ms | 2,900 ms | 340 ms | 0.05 |
Three findings worth more than the raw numbers:
The tiers differ mostly in their tails. Median-to-median, ISP is 10% slower than datacenter — irrelevant for almost any workload. But the p99 gap (340 ms vs 430 ms vs 2,900 ms) is a timeout-policy decision. A pipeline with a 2-second timeout will experience residential as "occasionally failing" when nothing is actually wrong; the requests were simply still in flight.
Connection reuse is the hidden tax of rotation. The residential reuse rate of 0.05 is not a defect — rotating exits mean every request pays the full TCP + TLS handshake cost, every time. On origins where TLS handshakes are expensive (large certificate chains, OCSP checks), that is 200–400 ms per request of pure protocol overhead. This is the mechanical reason sticky sessions feel "faster" beyond any reputation effect, and why ISP exits with stable addressing punch above their weight on multi-request flows.
Jitter is a capacity-planning input, not a trivia stat. High-jitter tiers make your concurrency math lie. If you size workers assuming 500 ms per request and the distribution says p99 is 2,900 ms, your workers pile up behind stragglers, memory grows, and your timeouts start firing under load even though a single-request benchmark looked fine. When the harness shows jitter above ~3× the median, either raise timeouts for that tier or route its traffic through a queue with burst headroom.
Decompose the tail before you fight it
When a tier shows a fat p99, the next question is which phase of the request is slow, because the fix differs completely by phase. DNS resolution delays point at the gateway's resolver; TCP connect delays point at routing distance and NAT churn; TLS handshake delays point at certificate chain size and OCSP behavior; and a slow first byte after the handshake points at the origin itself sizing you up (or an inline bot-detection vendor adding its verdict time). You can split these phases with a raw socket probe:
import socket
import ssl
import time
def phase_probe(host: str, port: int = 443, proxy=None) -> dict:
"""Measure DNS / TCP / TLS phases separately for one destination."""
t = {}
t0 = time.perf_counter()
addr = socket.gethostbyname(host)
t["dns_ms"] = (time.perf_counter() - t0) * 1000
t0 = time.perf_counter()
sock = socket.create_connection((addr, port), timeout=15)
t["tcp_ms"] = (time.perf_counter() - t0) * 1000
t0 = time.perf_counter()
ctx = ssl.create_default_context()
tls = ctx.wrap_socket(sock, server_hostname=host)
t["tls_ms"] = (time.perf_counter() - t0) * 1000
tls.close()
return {k: round(v) for k, v in t.items()}
Run it through each tier's gateway and the tail stops being mysterious. In my runs, residential p99 stragglers decomposed mostly into TCP connect time — new exit, new route, occasionally a transcontinental detour — while ISP exits showed TLS-heavy tails on origins with bot-detection vendors inline. Those are different problems: one you route around (stickiness), the other you budget for (timeouts).
Turning the profile into routing policy
The harness output maps directly to tier assignment rules. Flows that are latency-sensitive and multi-request (interactive fetches, paginated API walks, anything user-facing) belong on the tier with the best reuse rate and tightest p99 — usually ISP or datacenter-with-good-reputation. Bulk background collection tolerates fat tails and belongs wherever cost per successful row is lowest, which is often residential despite the timing. And any per-tier timeout you configure should be derived from that tier's measured p99 plus headroom, not copied from a blog post — including this one.
Re-run the harness monthly, and after any provider-side change. The performance envelope of an exit tier is not a constant of nature; it is a property of peering agreements, pool composition, and the hour of the day. The harness costs ten minutes to run. The timeout misconfigurations it prevents cost whole nights.
Top comments (0)