๐ŸŽ‰ Premium Proxies ยท 24-Hour Free TrialClaim Now
Use Cases

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.

S SparkProxy 0 24 min read
Share

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 saysWhat the browser showsWhat curl printsFirst 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 bodyStale 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 bodyWAF 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 carrierHow close it getsCatch
Carrier-targeted session from a mobile proxy providerSame operator ASN, same CGNAT egress poolProvider's own DNS and IP-family choices sit between you and the carrier
Prepaid SIM on that carrier in a hotspot or USB modemReal handset path, including the carrier's DNS and IPv6You have to be in the country
The customer runs your capture scriptTheir exact device and settingsSlow, 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.

Free trial

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:

CarrierDatacenterResidentialWhere the fault most likely sits
FailsWorksWorksSomething keyed to the carrier: its routing, its resolver, its IP range in your WAF
FailsFailsWorksA rule against non-consumer traffic plus something on mobile; check bot and WAF rules first
FailsFailsFailsYour own stack in that country: origin, CDN config, geo rule
WorksWorksWorksThe carrier's egress path is fine; look at the handset side (IPv6-only path, DNS64, Private DNS, the app)
Wrong markerRight markerRight markerThe 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.

PieceWhat you payRough size of one investigation
SparkProxy datacenter controlStarting from $75/month, plans are packs with unlimited bandwidth, so the unit is the plan and its concurrency, not trafficA 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 515 credits per request; the 1,000 free credits cover about 66 requests
Carrier exit from a mobile providerUsually billed per GBA few hundred page loads are small; check the provider's minimum
Prepaid SIMOne local top-upAlso 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 dig output from each resolver you queried.
  • The curl -v trace 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-ray IDs and edge codes, and tracepath output 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.

Special Discount ยท 20% off

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

Claim Discount

About the Author

Written by the SparkProxy Technical Team. SparkProxy runs a datacenter proxy network of 1M+ premium IPs across 80+ countries, with Random Rotation and Sticky Sessions over HTTP, HTTPS and SOCKS5, plus a Scraping API with residential routing. We don't sell mobile or carrier-targeted proxies, which is why this guide tells you where to get that exit rather than pretending we have one. Corrections are welcome at support@sparkproxy.io.

Keep reading

Related articles

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.

SparkProxyยทUse Cases