Why cellular IP addresses do not behave like fixed home broadband
When backend engineers write IP-based rate limiters, fraud velocity rules, or city-level localization logic, they usually test from a laptop connected to fixed fiber-to-the-home (FTTH) or cable broadband. On fixed residential broadband, the ISP assigns a dedicated /32 public IPv4 address (or a /56 IPv6 prefix) to the household router, terminated at a local neighborhood Broadband Network Gateway (BNG). That IP address represents a single household and remains anchored to one city with a physical radius of 5 to 20 kilometers.
Cellular 4G LTE and 5G Standalone (SA) networks operate on a completely different packet routing architecture. When a smartphone in a regional town connects to a local 5G radio cell tower (gNodeB), its packets do not egress onto the public internet at that cell tower.
Instead, the radio base station encapsulates the subscriber's IP packets inside a GPRS Tunneling Protocol (GTP-U) tunnel and backhauls them across the mobile operator's private MPLS/optical core network to a centralized User Plane Function (UPF in 5G) or Packet Data Network Gateway (PGW in 4G). A single nationwide mobile carrier in Germany, the United States, or Japan might operate 40,000 radio cell towers, but only 6 to 15 regional UPF/PGW internet breakout centers across the entire country!
Even more critically, because mobile carriers have hundreds of millions of active handsets and a limited pool of public IPv4 addresses, the regional UPF/PGW runs Carrier-Grade NAT (CGNAT / NAT44 / NAT64), multiplexing 2,000 to 8,000 simultaneous smartphones onto a single public IPv4 address.
If your backend architecture ignores cellular CGNAT and regional UPF routing, you will inevitably ship three severe production bugs:
- Mass mobile customer lockouts during traffic spikes (collateral damage from per-IP rate limiting).
- False "impossible travel" security alerts when the 5G core re-anchors a subscriber's GTP tunnel between two regional UPFs.
- Over-confident city geolocation (treating a regional mobile gateway in Munich as proof that the user is standing in downtown Munich rather than 120 km away in Nuremberg).
This engineering deep dive explains the packet flow of 4G/5G CGNAT, shows how ipscope models cellular subnets using connection.is_mobile and calibrated geo.accuracy_radius_km, and provides production Go and TypeScript patterns for CGNAT-safe rate limiting and session security.
Inside the 4G/5G mobile packet core: Why your server only sees the UPF/PGW
Let us trace a single HTTPS request from a 5G smartphone located in Augsburg, Germany to your API server:
[ Smartphone in Augsburg ]
| Internal Carrier IP (100.64.18.42 / RFC 6598 Shared CGNAT Space)
v
[ Local 5G Cell Tower (gNodeB in Augsburg) ]
| Encapsulates packet in GTP-U (UDP Port 2152) over Carrier Core MPLS
v
[ Regional 5G User Plane Function (UPF) in Munich (80 km away) ]
| 1. Decapsulates GTP-U header
| 2. CGNAT translates 100.64.18.42:54112 -> Public IP 203.0.113.88:41029
| (Port-Block Allocation: 64 ports per subscriber, ~1,000-4,000 users/IP)
v
[ Public Internet / DE-CIX Munich Peering ]
| Source IP seen by your server: 203.0.113.88
v
[ Your API Gateway / Load Balancer ]Two physical facts emerge from this packet diagram:
- The public internet only ever interacts with the UPF in Munich (
203.0.113.88): Every traceroute, BGP route collector, and active RTT probe on the public internet terminates at the carrier's regional UPF firewall in Munich. The 80 km radio and GTP-U backhaul leg between Augsburg and Munich is completely invisible to Layer 3 internet routing. - Port-Block Allocation (
PBA/ RFC 6888) packs thousands of users onto203.0.113.88: By allocating a deterministic block of 16 to 64 ephemeral source ports (1024..65535) per active handset, a single/32public IPv4 address on a carrier CGNAT pool serves thousands of unrelated people riding the train, shopping, or browsing your app at the same second.
How ipscope models cellular CGNAT pools accurately
When you query an IP address belonging to a 4G/5G carrier pool in ipscope, the response explicitly flags the cellular access topology (connection.connection_type: "mobile", connection.is_mobile: true) and widens geo.accuracy_radius_km to match the true geographic service area of the regional UPF/PGW:
{
"ip": "203.0.113.88",
"ip_version": 4,
"matched_network": "203.0.113.0/24",
"geo": {
"country_code": "DE",
"declared_country_code": "DE",
"physical_country_code": "DE",
"is_virtual_location": false,
"location_type": "physical",
"confidence": 0.91,
"subdivision": "Bavaria",
"city": "Munich",
"latitude": 48.1351,
"longitude": 11.582,
"accuracy_radius_km": 120,
"timezone": "Europe/Berlin"
},
"network": {
"cidr": "203.0.113.0/24",
"asn": 3320,
"organization": "Deutsche Telekom AG",
"rir": "RIPE"
},
"connection": {
"connection_type": "mobile",
"is_residential": false,
"is_mobile": true,
"is_business": false,
"is_hosting": false
},
"security": {
"is_vpn": false,
"is_hosting": false,
"is_tor_exit": false,
"is_proxy": false,
"anonymizer_type": "none",
"confidence": 0.99,
"reason_codes": []
}
}How the ipscope compiler separates fixed broadband from mobile CGNAT inside the same ISP ASN
Many incumbent telecom operators (such as Deutsche Telekom AS3320, Orange AS3215, AT&T, Vodafone, or NTT) operate both fixed fiber/DSL residential broadband and nationwide 4G/5G cellular networks under the same parent ASN or sibling ASNs. An ASN-only lookup cannot tell whether 203.0.113.0/24 is a fixed FTTH neighborhood router block (accuracy_radius_km: 12) or a 5G mobile CGNAT gateway (accuracy_radius_km: 120).
Our compiler classifies /24 IPv4 and /48 IPv6 subnets within dual-play telecom ASNs using four subnet-level signals:
- Carrier rDNS & RPSL
netnamePool Tokens: Telecom engineers segregate BNG fixed pools (dslb-,ftth-,bng-,static-) from mobile packet gateway pools (cgn-,pgw-,ggsn-,upf-,lte-,mobile-,gprs-) in bothPTRnaming templates and RIRnetname/descrattributes. - TCP / IP Stack & Passive MTU/MSS Fingerprinting: Fixed Ethernet/PPPoE broadband typically negotiates a
1492or1500byte MTU (TCP MSS 1452or1460), whereas cellular GTP-U encapsulation overhead and mobile radio bearer framing consistently clamp TCP Maximum Segment Size (MSS) to cellular-specific values (1360to1420bytes). - Ephemeral Port Allocation Entropy: Fixed home routers allocate outbound source ports sequentially or across the full
32768..60999Linux/macOS ephemeral range for a small number of devices, whereas carrier CGNAT appliances exhibit Port-Block Allocation (PBA) stepping across thousands of concurrent TLS sessions. - RTT Variance Distribution: Even when anchored to a Munich UPF, the tail distribution of radio access network (RAN) jitter across a
/24cellular pool has a distinct signature compared to a passive optical network (GPON) fiber termination.
Typical accuracy_radius_km across the four connection_type classes
connection.connection_type | Boolean Flag | Typical accuracy_radius_km | Physical Network Termination Point |
|---|---|---|---|
hosting | is_hosting: true | 5 to 10 km | Exact cloud region or colocation data center facility |
business | is_business: true | 5 to 15 km | Dedicated enterprise DIA / leased fiber termination |
residential | is_residential: true | 10 to 35 km | Local neighborhood BNG / CMTS serving a city or suburb |
mobile | is_mobile: true | 80 to 250 km | Regional 4G/5G Packet Gateway (PGW / UPF) serving a state or multi-city region |
Production Pattern 1: CGNAT-safe composite rate limiting in TypeScript
If your API gateway enforces a flat 60 requests / minute per IP limit, a single busy train station or stadium where 150 users on the same mobile carrier open your app will exhaust the IP bucket in seconds and return 429 Too Many Requests to every user on that carrier.
To fix this cleanly:
- For authenticated requests, always key your primary rate limiter by
userIdorsessionIdrather than raw IP. - For unauthenticated endpoints (catalog browsing, public search, OAuth login initiation), dynamically scale the IP bucket when
connection.is_mobile === trueand combine the IP with client-level entropy (such as an HTTP-only device cookie or TLSJA4fingerprint):
import { IpScopeClient } from '@ipscope/sdk-node';
const ipscope = new IpScopeClient({
apiKey: process.env.IPSCOPE_KEY!,
maxCacheEntries: 16_384,
cacheTtlMs: 300_000,
});
export interface RateLimitConfig {
bucketKey: string;
maxRequestsPerMinute: number;
windowSeconds: number;
}
export async function resolveRateLimitPolicy(
clientIp: string,
deviceCookieId: string | undefined,
tlsJa4Fingerprint: string | undefined,
): Promise<RateLimitConfig> {
const res = await ipscope.lookupIp(clientIp);
const { connection, security, security_extensions } = res;
const velocity = security_extensions?.abuse_velocity_score ?? 0;
// 1. High-velocity scanner or active proxy: strict per-IP quarantine bucket
if (velocity >= 75 || security.is_tor_exit || security.is_proxy) {
return {
bucketKey: `rl:strict:${clientIp}`,
maxRequestsPerMinute: 10,
windowSeconds: 60,
};
}
// 2. Cellular 4G/5G CGNAT pool: partition bucket by (IP + device/TLS sub-key)
// and raise the aggregate IP ceiling so shared carrier NAT never starves legitimate users
if (connection?.is_mobile && !security.is_vpn) {
const subKey = deviceCookieId || tlsJa4Fingerprint || 'anon';
return {
bucketKey: `rl:mobile:${clientIp}:${subKey}`,
maxRequestsPerMinute: 180,
windowSeconds: 60,
};
}
// 3. Standard residential or business broadband: per-IP bucket
return {
bucketKey: `rl:std:${clientIp}`,
maxRequestsPerMinute: 120,
windowSeconds: 60,
};
}Production Pattern 2: Uncertainty-aware "Impossible Travel" detection in Go
Many enterprise IAM and zero-trust authentication systems run an Impossible Travel detector: if a user's session token is used from City A at 10:00:00 and from City B (350 km away) at 10:02:00, the system computes an apparent travel speed of 10,500 km/h (faster than a commercial jet) and revokes the user's session.
On 4G/5G mobile networks, this naive calculation causes constant false logouts. Why?
- A user is sitting on a high-speed train or driving between two cities, or the mobile carrier's core network performs a routine load-balancing handoff from the Frankfurt UPF (
accuracy_radius_km: 150) to the Cologne/Dusseldorf UPF (accuracy_radius_km: 140). - Even worse, if the user switches from Wi-Fi (
residential,accuracy_radius_km: 15) to 5G cellular (mobile,accuracy_radius_km: 180) on the exact same phone, the IP centroid jumps160 kmin2 seconds!
Instead of measuring the distance between the two point centroids (lat_1, lon_1) and (lat_2, lon_2), you must compute the minimum physically required distance between their uncertainty circles by subtracting both accuracy_radius_km values from the Haversine great-circle distance:
D_effective = max(0, D_haversine(p1, p2) - R1 - R2)If the two uncertainty circles overlap (D_haversine <= R1 + R2), then D_effective = 0 km, proving that the user could physically be in the exact same spot while switching between Wi-Fi and 5G or experiencing a carrier UPF handoff!
Here is the complete Go implementation:
package sessionsec
import (
"math"
"time"
)
type GeoObservation struct {
ObservedAt time.Time
CountryCode string
Latitude float64
Longitude float64
AccuracyRadiusKm float64
IsMobile bool
IsVPN bool
}
// EffectiveTravelDistanceKm computes the minimum distance between two IP geolocation
// uncertainty circles on the WGS84 sphere.
func EffectiveTravelDistanceKm(prev, curr GeoObservation) float64 {
const earthRadiusKm = 6371.0
dLat := (curr.Latitude - prev.Latitude) * math.Pi / 180.0
dLon := (curr.Longitude - prev.Longitude) * math.Pi / 180.0
lat1 := prev.Latitude * math.Pi / 180.0
lat2 := curr.Latitude * math.Pi / 180.0
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(lat1)*math.Cos(lat2)*math.Sin(dLon/2)*math.Sin(dLon/2)
centroidDistanceKm := 2 * earthRadiusKm * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
// Subtract both uncertainty radii (plus a 50km carrier UPF handoff buffer if either leg is mobile)
mobileBufferKm := 0.0
if prev.IsMobile || curr.IsMobile {
mobileBufferKm = 50.0
}
effective := centroidDistanceKm - prev.AccuracyRadiusKm - curr.AccuracyRadiusKm - mobileBufferKm
if effective < 0 {
return 0
}
return effective
}
// IsImpossibleTravel returns true only when the effective minimum distance between
// uncertainty circles exceeds 900 km/h (commercial aircraft speed).
func IsImpossibleTravel(prev, curr GeoObservation) bool {
if EffectiveTravelDistanceKm(prev, curr) == 0 {
return false
}
hours := curr.ObservedAt.Sub(prev.ObservedAt).Hours()
if hours <= 0.05 {
// Enforce a minimum 3-minute floor to avoid division spikes
hours = 0.05
}
requiredSpeedKmh := EffectiveTravelDistanceKm(prev, curr) / hours
return requiredSpeedKmh > 900.0
}By subtracting prev.AccuracyRadiusKm and curr.AccuracyRadiusKm, switching from home fiber in Augsburg (R_1 = 15 km) to a Munich 5G UPF (R_2 = 120 km, centroid distance 65 km) yields EffectiveTravelDistanceKm = 0 km, eliminating 100% of false Wi-Fi-to-5G impossible-travel alerts while still catching real credential theft across continents (D_effective > 4,000 km).
How IPv6 changes mobile rate limiting (but not UPF city radius)
Most major 4G/5G carriers now deploy dual-stack or IPv6-mostly (RFC 8925 / 464XLAT) cellular networks where each smartphone receives its own globally routable /64 IPv6 prefix.
Understanding what IPv6 changes on mobile networks is critical for systems architects:
- IPv6 eliminates CGNAT port sharing across subscribers: Two different smartphones on an IPv6 cellular network will never share the same
/64IPv6 prefix. When your rate limiter keys IPv6 requests by the/64prefix (rather than/128, since iOS and Android rotate temporary RFC 8981 privacy interface identifiers within their assigned/64), every mobile subscriber gets an isolated rate-limit bucket! - IPv6 does NOT change the physical UPF/PGW geolocation radius: Even on IPv6, the subscriber's
/64prefix is dynamically delegated out of a large/36or/40pool anchored at the regional 5G UPF in Munich, Frankfurt, or Dallas. Therefore,connection.is_mobileremainstrueandgeo.accuracy_radius_kmremains80to250 km.
Frequently asked questions (FAQ)
Why should I rate-limit IPv6 traffic by /64 instead of /128?
Modern mobile and desktop operating systems (iOS, Android, macOS, Windows, Linux) implement RFC 8981 Temporary Address Extensions, which periodically generate new random 64-bit interface identifiers inside the subscriber's assigned /64 subnet. Furthermore, anyone allocated a /64 has 2^64 (18.4 quintillion) individual /128 addresses at their disposal. Always normalize IPv6 addresses to their /64 prefix when keying rate-limit counters.
Can any IP geolocation provider resolve the exact cell tower a 5G phone is connected to?
No. Without on-device GPS/Wi-Fi permissions (navigator.geolocation in the browser or CoreLocation/FusedLocationProvider in a native mobile app), a remote web server only sees the source IP address of the carrier's internet breakout gateway (UPF / PGW). Any IP geolocation vendor claiming 5 km street-level accuracy on cellular IP ranges is reporting false precision. That is why ipscope provides an honest, physics-backed accuracy_radius_km alongside connection.is_mobile: true.