Site Fails on Mobile Data Only? Reproduce It With Proxies
Website not loading on mobile data but works on wifi? Reproduce the failure on the carrier's network path, then isolate DNS, WAF, IPv6, MTU and CDN causes.
A support ticket that says the website not loading on mobile data but works on wifi is one of the hardest to close, because every test you run from the office succeeds. The customer sees a spinner, then "This site can't be reached", only on one carrier, and your phone on a different network loads the page instantly. This guide is a four-rung ladder: get your requests onto that carrier's path, run the same requests from control exits, split DNS from routing, then check the remaining causes one at a time. You finish with an evidence bundle a carrier or CDN will actually act on.
The symptom, in the customer's words and in curl's
Translate the report into a network layer before touching any tooling. Ask the customer for the browser string, or capture the curl one yourself once you're on their path.
| What the customer says | What the browser shows | What curl prints | First layer to suspect |
|---|---|---|---|
| "It just spins, then gives up" | Chrome `ERR_CONNECTION_TIMED_OUT`, "took too long to respond" | `curl: (28) Connection timed out after 30001 milliseconds` | Routing, filtering, RPKI, firewall |
| "It says the site doesn't exist" | `ERR_NAME_NOT_RESOLVED` | `curl: (6) Could not resolve host: www.sparkproxy.io` | DNS |
| "It loads the old site" or "certificate warning" | `NET::ERR_CERT_COMMON_NAME_INVALID` or a stale page | `curl: (60)` certificate problem, or the old server's body | Stale DNS pointing at a previous host |
| "Blank page, half the images missing" | Page loads, assets time out | `curl: (28) Operation timed out after 45001 milliseconds with 0 bytes received` | MTU, CDN edge, one asset host |
| "A Cloudflare page saying rate limited" | `Error 1015: You are being rate limited` | HTTP 429 or a 1015 HTML body | WAF or rate rule on a shared IP |
| "Connection reset" | `ERR_CONNECTION_RESET` | `curl: (35) Recv failure: Connection reset by peer` | TLS middlebox, carrier filter |
The meaning of each curl number comes from libcurl's own error list: 6 is "Could not resolve host", 7 is "Failed to connect() to host or proxy", 28 is "Operation timeout", 35 is "A problem occurred somewhere in the SSL/TLS handshake" and 56 is "Failure with receiving network data" (libcurl error codes). The 1015 text is Cloudflare's, which describes it as a rate limiting rule the site owner configured (Cloudflare error 1015).
Two of these show up only through a proxy, so learn them now and don't misread them later. curl: (56) CONNECT tunnel failed, response 502 means the proxy itself couldn't open a connection to your server, which is a finding, not a proxy bug. curl: (7) against the proxy's own host means your credentials or port are wrong. We produced the 56 string ourselves on curl 8.15.0 against a local test proxy that answered every CONNECT with 502.
Rung 1: put your requests on the carrier's path
Nothing downstream is worth doing until a request of yours fails the way the customer's does, which means leaving the internet through the same operator in the same country.
A disclosure that shapes this rung: SparkProxy sells datacenter proxies, not mobile or carrier-targeted ones. For the carrier exit you need one of three things.
| Way onto the carrier | How close it gets | Catch |
|---|---|---|
| Carrier-targeted session from a mobile proxy provider | Same operator ASN, same CGNAT egress pool | Provider's own DNS and IP-family choices sit between you and the carrier |
| Prepaid SIM on that carrier in a hotspot or USB modem | Real handset path, including the carrier's DNS and IPv6 | You have to be in the country |
| The customer runs your capture script | Their exact device and settings | Slow, and depends on their patience |
How providers implement carrier filters, and why MVNO labels mislead, is covered in our guide to carrier-targeted mobile proxies. Here, the only rule that matters is: hold one IP for the whole capture. Ask for a sticky session so curl, the browser capture and every repeat leave from the same carrier address. If each tool rotates to a different IP, you can't tell a flaky address from a flaky server.
Confirm the exit before you trust a single result. This script asks a public echo service for the exit IP through your proxy, then looks up the announcing prefix and ASN on RIPEstat's network-info endpoint. Run it with the name of an environment variable holding the proxy URL:
# exit_info.py usage: python exit_info.py PROXY_CARRIER
import os
import sys
import requests
def exit_info(proxy_url):
proxies = {"http": proxy_url, "https": proxy_url}
ip = requests.get("https://api.ipify.org", proxies=proxies, timeout=30).text.strip()
data = requests.get(
"https://stat.ripe.net/data/network-info/data.json",
params={"resource": ip},
timeout=20,
).json()["data"]
return {"exit_ip": ip, "prefix": data.get("prefix"), "asns": data.get("asns", [])}
if __name__ == "__main__":
print(exit_info(os.environ[sys.argv[1]]))
If the ASN isn't the carrier your customer named, stop and fix the session before collecting anything. Then capture curl's view through that sticky exit. The -w line records each phase, and the verbose trace goes to a timestamped file you'll attach to the escalation later:
export PROXY_CARRIER='http://USER-session-ticket4821:PASS@MOBILE_HOST:MOBILE_PORT'
TS=$(date -u +%Y%m%dT%H%M%SZ)
curl -sv -o /dev/null --max-time 45 -x "$PROXY_CARRIER" \
-w 'connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total} code=%{http_code}\n' \
https://www.sparkproxy.io/ 2> "carrier-$TS.curl.txt"
One trap in that output: through a proxy, curl's time_connect and remote_ip describe the hop to the proxy, not to your server. The phase that matters is how long the tunnel plus TLS took (time_appconnect) and whether a first byte ever arrived.
Then record the browser's view with a HAR file, which captures every subresource, so a page whose HTML loads while one asset host times out shows up plainly. Playwright's new_context takes a proxy and a record_har_path, and its docs warn that you must "call browser_context.close() for the HAR to be saved" (Playwright Browser API). The current release on PyPI on 30 October 2026 was 1.64.1, uploaded 22 October 2026, requiring Python 3.10 or newer.
# capture_har.py usage: python capture_har.py https://www.sparkproxy.io/ carrier
import os
import sys
import time
from urllib.parse import unquote, urlsplit
from playwright.sync_api import Error as PlaywrightError
from playwright.sync_api import sync_playwright
url, label = sys.argv[1], sys.argv[2]
p_url = urlsplit(os.environ["PROXY_" + label.upper()])
proxy = {
"server": f"{p_url.scheme}://{p_url.hostname}:{p_url.port}",
"username": unquote(p_url.username or ""),
"password": unquote(p_url.password or ""),
}
stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
har_path = f"{label}-{stamp}.har"
with sync_playwright() as pw:
browser = pw.chromium.launch()
context = browser.new_context(proxy=proxy, record_har_path=har_path)
page = context.new_page()
exit_ip = page.goto("https://api.ipify.org", timeout=30000).text().strip()
try:
resp = page.goto(url, timeout=45000, wait_until="load")
outcome = f"HTTP {resp.status}" if resp else "no response"
except PlaywrightError as exc:
outcome = str(exc).splitlines()[0] # e.g. net::ERR_CONNECTION_TIMED_OUT
context.close()
browser.close()
print(f"{stamp}\t{label}\t{exit_ip}\t{outcome}\t{har_path}")
The unquote calls matter if your password contains @ or :, which have to be percent-encoded inside the proxy URL but not in Playwright's separate fields.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Rung 2: replay through control exits
A carrier failure proves little alone. You need the same request, minutes apart, from addresses that differ in one way.
A datacenter exit in the same country. The SparkProxy gateway is gateway.sparkproxy.io: port 11000 gives Random Rotation (a fresh IP per request), port 11002 gives Sticky Sessions (one IP held for 5 minutes), and port 13000 is SOCKS5. The country goes in the username in the form the homepage shows, user-country-us; copy the exact string from your dashboard. We confirmed all three ports accepted TCP connections on 30 October 2026, though we had no account credentials on the test machine, so no proxied page load is reported here.
A residential exit in the same country. The SparkProxy Scraping API's premium_proxy=true routes "through a residential proxy from the premium pool", and country_code picks the exit country (Scraping API docs). Use render_js=false for a plain fetch so the timing reflects the network, not Chromium.
The script runs the three exits one after another, never in parallel. Each run checks for a marker string that only your current origin prints (a build ID in the HTML, or a line on a health endpoint), which tells you which server answered:
# compare_exits.py run all three exits sequentially against one URL
import base64
import os
import time
import requests
URL = "https://www.sparkproxy.io/healthz"
MARKER = os.environ.get("ORIGIN_MARKER", "build=2026.10.30")
def via_proxy(label):
proxy = os.environ["PROXY_" + label.upper()]
start = time.monotonic()
try:
r = requests.get(URL, proxies={"http": proxy, "https": proxy}, timeout=(10, 30))
ray = r.headers.get("cf-ray", "")
return label, r.status_code, round(time.monotonic() - start, 2), ray, MARKER in r.text
except requests.RequestException as exc:
return label, type(exc).__name__, round(time.monotonic() - start, 2), "", False
def via_api_residential(country):
r = requests.get(
"https://scrape.sparkproxy.io/api/v1",
headers={"X-API-Key": os.environ["SPARK_API_KEY"]},
params={"url": URL, "render_js": "false", "premium_proxy": "true",
"country_code": country, "json_response": "true"},
timeout=90,
)
r.raise_for_status()
envelope = r.json()
body = base64.b64decode(envelope["body"]).decode("utf-8", "replace")
seconds = round(envelope["duration_ms"] / 1000, 2)
return "resi-api", envelope["status_code"], seconds, "", MARKER in body
if __name__ == "__main__":
for row in (via_proxy("carrier"), via_proxy("dc"), via_api_residential("DE")):
print(*row, sep="\t")
Set PROXY_DC to something like http://USER-country-de:PASS@gateway.sparkproxy.io:11002. Note how the residential row is read: json_response=true is what wraps the result in an envelope with job_id, status_code, duration_ms, credits_used and a body field, and that body is base64. Search for the marker only after decoding it, or every check comes back false. When we called the API from our test machine on 30 October 2026 it returned HTTP 521, so no live API row appears in this post.
Read the three rows together:
| Carrier | Datacenter | Residential | Where the fault most likely sits |
|---|---|---|---|
| Fails | Works | Works | Something keyed to the carrier: its routing, its resolver, its IP range in your WAF |
| Fails | Fails | Works | A rule against non-consumer traffic plus something on mobile; check bot and WAF rules first |
| Fails | Fails | Fails | Your own stack in that country: origin, CDN config, geo rule |
| Works | Works | Works | The carrier's egress path is fine; look at the handset side (IPv6-only path, DNS64, Private DNS, the app) |
| Wrong marker | Right marker | Right marker | The carrier exit is reaching an old server: go straight to rung 3 |
If the carrier exit loads your site perfectly, a proxy has reproduced all it can, and the remaining difference is the handset. Our post on proxies for mobile app testing teams lists what an egress proxy cannot imitate about a phone.
Sampling many carrier IPs instead of one
Your sticky exit is one address out of the carrier's pool. Mobile operators put many subscribers behind each public IPv4 address using carrier-grade NAT; the internal side commonly uses the RFC 6598 range 100.64.0.0/10, which the RFC calls "Shared Address Space" (RFC 6598). Our explainer on CGNAT and proxies covers the mechanics. What matters here is the question it raises: does every public IP in the carrier's range fail, or only some?
Switching the carrier session to random rotation answers it, with one practical wrinkle. With pure per-request rotation, the IP that answers an echo service and the IP that hits your site are different, so you can't pair an outcome with an address from the client side. Either make your health endpoint echo the client IP it saw, or mint a new sticky session per sample, which gives you a fresh IP that stays put for both requests. The script does the second. Your provider's session syntax varies, so the session ID goes in as a {session} placeholder:
# carrier_sample.py one new sticky session per sample, results grouped by /24
import collections
import ipaddress
import os
import uuid
import requests
TEMPLATE = os.environ["PROXY_CARRIER_TEMPLATE"] # ...USER-session-{session}:PASS@MOBILE_HOST:PORT
URL = "https://www.sparkproxy.io/healthz"
SAMPLES = 30
by_net = collections.defaultdict(lambda: {"ok": 0, "fail": 0, "ips": set()})
for _ in range(SAMPLES):
proxy = TEMPLATE.format(session=uuid.uuid4().hex[:10])
proxies = {"http": proxy, "https": proxy}
try:
ip = requests.get("https://api.ipify.org", proxies=proxies, timeout=20).text.strip()
except requests.RequestException:
continue
net = str(ipaddress.ip_network(ip + "/24", strict=False)) if ":" not in ip else ip
try:
ok = requests.get(URL, proxies=proxies, timeout=(10, 30)).status_code < 400
except requests.RequestException:
ok = False
by_net[net]["ok" if ok else "fail"] += 1
by_net[net]["ips"].add(ip)
for net, row in sorted(by_net.items(), key=lambda kv: -kv[1]["fail"]):
print(f"{net:20} ok={row['ok']:2} fail={row['fail']:2} distinct_ips={len(row['ips'])}")
Read the pattern, not the totals:
- Every network fails: the fault follows the carrier, not an address. Think routing, carrier DNS, RPKI or a carrier-side filter.
- Failures cluster in a few /24s: something has scored those specific addresses. That's almost always a WAF reputation list, a rate rule or a blocklist, and it's your problem to fix, not the carrier's.
- Random, scattered failures: congestion or a flaky provider device. Re-run at a different hour before concluding anything.
Rung 3: separate name resolution from routing
DNS goes first among the specific causes because it's the cheapest to check and the most common thing to change recently. If you moved hosts, switched CDN or edited records in the last few days, a resolver somewhere may still hold the old answer. RFC 8767 also lets resolvers deliberately serve expired data when they can't reach your authoritative servers: it defines "a method (serve-stale) for recursive resolvers to use stale DNS data" and "suggests a cap of 7 days" (RFC 8767).
Start by comparing what each resolver hands out against what your authoritative server says:
dig +short NS sparkproxy.io
dig +noall +answer www.sparkproxy.io A @jonah.ns.cloudflare.com # the truth, with TTL
for r in 8.8.8.8 1.1.1.1 9.9.9.9 CARRIER_RESOLVER_IP; do
printf '%-18s A: ' "$r"; dig +short www.sparkproxy.io A @"$r" | tr '\n' ' '
printf ' AAAA: '; dig +short www.sparkproxy.io AAAA @"$r" | tr '\n' ' '; echo
done
Many carrier resolvers won't answer queries from outside their own network, so CARRIER_RESOLVER_IP often only works from the prepaid SIM, or from the customer's phone using a DNS lookup app. A different answer from the authoritative one is the finding; a timeout from outside proves nothing.
Then pin the origin and compare against a normal request. Here's the part most guides get wrong: curl --resolve does nothing for your site's hostname when you go through an HTTP proxy. curl hands the hostname to the proxy in the CONNECT line and never resolves it locally. We checked this on curl 8.15.0 against a local proxy that logged every request. With --resolve www.sparkproxy.io:443:203.0.113.10, the proxy received CONNECT www.sparkproxy.io:443. With --connect-to, it received CONNECT 203.0.113.10:443. libcurl's docs explain why: when a connect-to target differs from the URL, "the HTTP proxy is automatically switched to tunnel mode" (CURLOPT_CONNECT_TO), and the curl manual notes the option "does NOT affect the hostname/port number that is used for TLS/SSL (e.g. SNI, certificate verification)" (curl manual). So your certificate still validates.
ORIGIN=203.0.113.10 # the IP your authoritative DNS returns today
# normal run: the exit resolves the name itself
curl -s -o /dev/null -w 'normal code=%{http_code} total=%{time_total}\n' \
--max-time 45 -x "$PROXY_CARRIER" https://www.sparkproxy.io/
# pinned run through an HTTP proxy: use --connect-to, not --resolve
curl -s -o /dev/null -w 'pinned code=%{http_code} total=%{time_total}\n' \
--max-time 45 -x "$PROXY_CARRIER" \
--connect-to www.sparkproxy.io:443:$ORIGIN:443 https://www.sparkproxy.io/
SOCKS5 works the other way around. The curl manual describes --socks5 as using the proxy "but resolve the hostname locally", and --socks5-hostname as letting "the proxy resolve the hostname". With a socks5:// proxy URL, --resolve applies again, because curl does the lookup and sends the proxy an address:
curl -s -o /dev/null -w 'socks5 local-dns code=%{http_code}\n' \
-x 'socks5://USER-country-de:PASS@gateway.sparkproxy.io:13000' \
--resolve www.sparkproxy.io:443:$ORIGIN https://www.sparkproxy.io/
Interpret the pair. If the normal run fails or shows the wrong marker and the pinned run works, the exit's resolver has a stale or broken answer. If both fail identically, DNS is cleared and the fault is on the path to $ORIGIN. One honest limit: an HTTP proxy's lookup uses whatever resolver the provider configured, which may or may not be the carrier's. That's why the dig comparison above, run from a real carrier connection, carries more weight than the proxy result for DNS. Our explainer on how proxies handle DNS resolution goes deeper on where lookups happen.
Rung 4a: blocks aimed at the carrier's addresses
With DNS cleared, check the causes that target the carrier's IPs.
Your WAF or rate limiter, hitting a shared address. Because one public CGNAT IP fronts many subscribers, a per-IP threshold tuned on broadband traffic trips far sooner on mobile. The symptom is the 1015 page, a 429, or a bot challenge that a webview can't solve. The sampling table from the previous section is the tell: failures clustered on a few /24s. Search your WAF events for those IPs at the timestamps you logged. On Cloudflare, every response carries a Ray ID in the cf-ray header, and its docs say all customers can find Ray IDs and the matching IP, user agent and ASN "through sampled logs in Security Events" (Cloudflare Ray ID). The fix is a higher threshold or a different rate key for that ASN, not an allowlist of one IP that will have changed by tomorrow.
The carrier refusing your route. Some networks drop BGP routes whose RPKI status is invalid. If your host announced your prefix from a new ASN after a migration and the ROA wasn't updated, networks that validate routes can't reach you at all, and the symptom is a clean connection timeout from that carrier only. Check it in seconds against RIPEstat:
# rpki_check.py usage: python rpki_check.py 203.0.113.10
import sys
import requests
BASE = "https://stat.ripe.net/data"
def rpki_status(ip):
info = requests.get(f"{BASE}/network-info/data.json",
params={"resource": ip}, timeout=20).json()["data"]
results = []
for asn in info.get("asns", []):
v = requests.get(f"{BASE}/rpki-validation/data.json",
params={"resource": asn, "prefix": info["prefix"]},
timeout=20).json()["data"]
results.append((info["prefix"], "AS" + asn, v.get("status")))
return results
if __name__ == "__main__":
for prefix, asn, status in rpki_status(sys.argv[1]):
print(prefix, asn, status)
valid or unknown rules this out. Anything starting with invalid is a ticket for your hosting provider, and it explains a carrier-only outage completely.
The carrier filtering your range or your category. Operators in some markets filter mobile data by default, and a newly assigned IP block can sit on outdated filter or bogon lists for a while. The symptom is a carrier block page, a reset during TLS (curl: (35)), or a timeout. A proxy reproduces this only if the provider's exit really sits on the carrier's filtered consumer path, which you can't assume; a prepaid consumer SIM is the reliable test. If it's a category filter, the carrier's site-reclassification form is the fix, not your server.
Rung 4b: path faults in IPv6, MTU and the CDN edge
These three are about the route packets take rather than who's blocked, and only part of each can be reproduced through a proxy.
IPv4 and IPv6 take different paths. Some mobile operators run IPv6-only access and give handsets IPv4 reachability through 464XLAT, which RFC 6877 describes as "providing limited IPv4 connectivity across an IPv6-only network" (RFC 6877). Those phones use your AAAA record natively. An office wifi that is IPv4-only never touches it, so a stale AAAA pointing at an old server, or a firewall that forgot IPv6, is invisible from the office. Happy Eyeballs won't always save you either: RFC 8305 races connection attempts with a recommended "Connection Attempt Delay" of 250 milliseconds (RFC 8305), so it falls back when an IPv6 connection never opens, but a connection that opens and then stalls is already committed.
A proxy exit is the wrong tool here, because the provider's software picks its own address family for the onward hop. Test IPv6 directly from any IPv6-capable machine:
dig +short www.sparkproxy.io AAAA
curl -6 -sv -o /dev/null --max-time 20 https://www.sparkproxy.io/ 2>&1 | grep -E 'Connected to|HTTP/|curl:'
curl -4 -sv -o /dev/null --max-time 20 https://www.sparkproxy.io/ 2>&1 | grep -E 'Connected to|HTTP/|curl:'
If you publish no AAAA at all, those carriers' DNS64 resolvers synthesise one; RFC 6147 defines DNS64 as "a mechanism for synthesizing AAAA records from A records" (RFC 6147). Your server then sees the carrier's NAT64 addresses, which loops straight back to the shared-IP rate limit problem above.
A TLS handshake that stalls on packet size. Mobile cores carry user traffic inside GTP-U tunnels (3GPP TS 29.281), and every tunnel header shrinks the space left for your packets. If something on your side drops the ICMP messages that tell the sender to shrink packets, large packets vanish: RFC 2923 catalogues this as "the long-standing black hole problem" (RFC 2923). The certificate chain is usually the first big packet, so on an OpenSSL build of curl the trace stops after * TLSv1.3 (OUT), TLS handshake, Client hello (1): and sits there until error 28. Probe from your server back toward the carrier exit IP:
EXIT_IP=198.51.100.44 # the carrier exit from exit_info.py
tracepath -n "$EXIT_IP" # watch the reported pmtu drop below 1500
ping -M do -s 1472 -c 3 "$EXIT_IP" # 1472 + 28 header bytes = 1500; shrink until replies return
Many mobile exits ignore ping, so silence isn't proof. The fixes are clamping MSS on your load balancer and allowing ICMP "fragmentation needed" (and ICMPv6 "packet too big") through your firewall. Our post on MTU and MSS clamping in proxy connections walks through the settings.
A CDN edge only that carrier reaches. Carriers peer with CDNs in particular cities, so their users can all land on one point of presence while everyone else lands elsewhere. If that edge has a stale cached object or a local fault, only that carrier sees it. On Cloudflare the cf-ray response header ends in a three-letter code that "identifies the data center processing the request" (Cloudflare HTTP headers); when we requested www.sparkproxy.io from our machine on 30 October 2026 it ended in -FRA. Compare the suffix from the carrier exit against your control exits. A different code on the failing exit is your lead. Other CDNs expose a similar edge header; check their documentation for its name.
Pacing, cost and false positives
Keep every run between one and three concurrent connections. Your carrier exit shares its public IP with real subscribers already using your site, and a burst from you on top of theirs can trip your own rate limiter, leaving you debugging a block you created. Label your test traffic with a request header such as X-Debug-Ticket: 4821 so you can pull exactly your requests out of WAF and origin logs.
| Piece | What you pay | Rough size of one investigation |
|---|---|---|
| SparkProxy datacenter control | Starting from $75/month, plans are packs with unlimited bandwidth, so the unit is the plan and its concurrency, not traffic | A 24-hour free trial with no credit card usually covers it |
| Scraping API residential control | `premium_proxy` costs 10 credits without JS; `country_code` adds 5 | 15 credits per request; the 1,000 free credits cover about 66 requests |
| Carrier exit from a mobile provider | Usually billed per GB | A few hundred page loads are small; check the provider's minimum |
| Prepaid SIM | One local top-up | Also lets you run `dig` against the carrier resolver |
Prices are in USD as shown on sparkproxy.io and its Scraping API docs on 30 October 2026.
The evidence bundle for an escalation
Carriers and CDNs act on evidence they can match against their own logs. Collect these before you open a ticket:
- Carrier and country, plus the exit's ASN and prefix from
exit_info.py. - Exit IP and exact UTC timestamps for each failing request, ideally three or more over an hour.
- Your origin IPs and DNS records, with TTLs, as the authoritative server returns them, plus the
digoutput from each resolver you queried. - The
curl -vtrace for the failing request, and the same request from a control exit for contrast. - The HAR file from
capture_har.py, showing which subresources failed. - The comparison table from rung 2, and the /24 sampling output if failures clustered.
- RPKI status of your prefix,
cf-rayIDs and edge codes, andtracepathoutput if you suspect MTU.
Route carrier tickets through your hosting provider's NOC or the carrier's peering contact rather than consumer support. Lead CDN tickets with the Ray IDs.
Frequently asked questions
FAQ
The usual reasons are stale DNS at the carrier's resolver after a move, a WAF or rate rule tripping on a shared CGNAT address, a broken IPv6 path that office wifi never uses, a packet-size black hole during TLS, or a CDN edge only that carrier reaches. Check them in that order, cheapest first.
No. A carrier-targeted proxy reproduces the carrier's network path from its egress to your server, including its IP range and routing. It doesn't reproduce the handset's own DNS settings, its IPv6-only path, the app, or anything the carrier does inside its radio network.
Because curl sends your hostname to the proxy in the CONNECT request and never resolves it locally, so the override is never consulted. Use --connect-to host:443:IP:443 instead, which switches the proxy into tunnel mode toward the pinned IP while keeping the hostname for SNI and certificate checks.
Run the same request twice through the carrier exit: once normally and once pinned to your current origin IP with --connect-to. If only the pinned run works, DNS is stale; if both fail, the path to your origin is the problem. Confirm with dig against the carrier resolver from a real carrier connection.
Yes, and it's a common cause. One public CGNAT address fronts many subscribers, so per-IP thresholds trip early. Sample many carrier IPs; failures that cluster on a few /24 ranges point at a reputation list or rate rule rather than at the carrier.
The carrier and ASN, exit IPs with UTC timestamps, your origin IPs and DNS records, a curl -v trace, a HAR file, control-exit results for comparison, your prefix's RPKI status and, for a CDN, the Ray IDs and edge codes.
Get 20% off your first month
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Save up to 15% more on quarterly, half-yearly and yearly plans
Related articles
BlueStacks Proxy and LDPlayer Proxy Setup That Sticks
BlueStacks proxy and LDPlayer proxy setup that keeps authentication: Proxifier on the host, adb http_proxy with an IP whitelist, or an in-emulator proxy app.
New Listing Alert Scraper: Rental Alerts in Minutes
Build a new listing alert scraper: poll saved rental searches every few minutes, detect new listing IDs, filter by price and beds, and push alerts to a phone.
FoxyProxy Setup With Username and Password
FoxyProxy setup for per-site routing: saved username and password, patterns that match, why Chrome rejects SOCKS5 auth, and how to stop the login popup.
