Geofencing & Geolocking in Applications: The Complete Guide
Ever tried watching a show on Netflix while traveling and got hit with "This content is not available in your region"? That's geolocking in action. And if you've ever gotten a push notification from Starbucks the moment you walked past one — that's geofencing.
These two concepts sound similar but serve very different purposes. In this guide, we'll break down both, explore the tech behind them, and walk through real implementations — from IP lookups to coordinate math to CDN-level blocking.
Geofencing vs Geolocking: What's the Difference?
Let's clear this up right away because these terms get mixed up constantly.
| Geofencing | Geolocking | |
|---|---|---|
| What it does | Triggers an action when a user enters/exits a geographic area | Restricts or allows access based on user location |
| Primary purpose | Engagement, notifications, analytics | Access control, compliance, licensing |
| Direction | "You entered zone X, here's a coupon" | "You're in country Y, you can't see this" |
| Typical precision | GPS-level (meters) | IP-level (country/region) |
| Examples | Uber surge zones, retail push notifications, fleet tracking | Netflix content libraries, GDPR compliance, regional pricing |
Geofencing defines a virtual perimeter around a real-world area. When a device enters or leaves that perimeter, something happens — a notification fires, data gets logged, a workflow triggers.
Geolocking (also called geo-restriction or geo-blocking) restricts access to content or services based on the user's geographic location. It's a gatekeeper, not a trigger.
Think of it this way:
Geofencing: "Welcome to the store! Here's 10% off." (reactive)
Geolocking: "Sorry, this store doesn't serve your country." (restrictive)
Why Would You Want Location-Based Restrictions?
There are more reasons than you'd think.
1. Content Licensing
This is the big one. Streaming platforms like Netflix, Spotify, and Disney+ license content on a per-region basis. A show available in the US might not be licensed for the UK. Geolocking enforces those contracts.
2. Regulatory Compliance
- GDPR (EU): Certain data processing rules apply to EU residents
- Data residency laws: Some countries (Russia, China, India) require user data to stay within their borders
- Online gambling: Legal in some jurisdictions, heavily restricted in others
- Financial services: Different regulations per country — you can't offer certain financial products everywhere
3. Regional Pricing
Software companies often adjust pricing by region. A $20/month subscription in the US might be $5/month in India. Without geolocking, everyone would just VPN to the cheapest region.
4. Security
Blocking traffic from regions where you have no customers can reduce your attack surface. If your SaaS only serves North America and Europe, traffic from unexpected regions might be worth flagging or blocking.
5. Feature Rollout
Rolling out a new feature? Test it in Canada first before going global. Geofencing lets you do staged rollouts by region without complex feature flag setups.
6. Marketing & Engagement
Retail chains use geofencing to send push notifications when customers are near a store. Ride-sharing apps define surge pricing zones. Event apps trigger check-ins when attendees arrive.
How Geolocation Actually Works
Before you can fence or lock anything, you need to know where the user is. There are several ways to figure that out, each with different tradeoffs.
IP-Based Geolocation
The most common server-side approach. Every device on the internet has an IP address, and databases map IP ranges to geographic locations.
User Request
|
v
[Your Server] --> Extract IP --> [GeoIP Database Lookup]
|
v
Country: US
Region: California
City: San Francisco
Lat/Long: 37.77, -122.42
Accuracy:
- Country level: ~99% accurate
- State/Region level: ~80-90% accurate
- City level: ~50-75% accurate
- Exact coordinates: Not reliable at all
Limitations:
- VPNs and proxies mask the real IP
- Mobile networks can show the carrier's gateway, not the user's location
- Corporate networks might route through a central office in another state
- IPv6 adoption is still uneven in geolocation databases
GPS-Based Geolocation
The gold standard for precision (within a few meters). Works through the device's GPS hardware. Primarily used in mobile apps, but the browser Geolocation API can access it too.
Pros: Extremely accurate, works offline (on mobile)
Cons: Requires user permission, drains battery, doesn't work well indoors, not available server-side
WiFi Triangulation
By measuring signal strength from nearby WiFi access points and comparing against a database of known access point locations, you can estimate position to within 15-40 meters.
Pros: Works indoors, no GPS needed
Cons: Requires a database of access points, accuracy varies, urban areas only
Cell Tower Triangulation
Similar to WiFi but uses cell towers. Less precise (100m to several km) but works anywhere with cell service.
Browser Geolocation API
The browser's built-in API that combines multiple sources (GPS, WiFi, IP) to get the best possible location:
// Browser Geolocation API
navigator.geolocation.getCurrentPosition(
(position) => {
console.log('Latitude:', position.coords.latitude);
console.log('Longitude:', position.coords.longitude);
console.log('Accuracy:', position.coords.accuracy, 'meters');
},
(error) => {
switch (error.code) {
case error.PERMISSION_DENIED:
console.log('User denied geolocation');
break;
case error.POSITION_UNAVAILABLE:
console.log('Location info unavailable');
break;
case error.TIMEOUT:
console.log('Request timed out');
break;
}
},
{
enableHighAccuracy: true, // Use GPS if available
timeout: 10000, // Wait up to 10 seconds
maximumAge: 300000 // Accept cached position up to 5 minutes old
}
);
Important: This requires explicit user permission. The browser shows a permission prompt, and users can deny it. You cannot silently get GPS-level location in a browser.
IP Geolocation Deep Dive
Since IP geolocation is the backbone of most server-side geo-restriction, let's go deeper.
Major GeoIP Database Providers
| Provider | Free Tier | Accuracy | Update Frequency | Notes |
|---|---|---|---|---|
| MaxMind GeoLite2 | Yes (free) | Good | Biweekly | Industry standard, requires account |
| MaxMind GeoIP2 | Paid | Better | Daily | Commercial version of GeoLite2 |
| IP2Location | Free (LITE) | Good | Monthly | Also has paid tiers |
| ipinfo.io | 50k req/mo free | Good | Continuous | API-based, no local DB needed |
| DB-IP | Free tier | Decent | Monthly | Also has paid tiers |
| Cloudflare | Included | Good | Continuous | Only if you use Cloudflare |
MaxMind GeoLite2 — The Industry Standard
MaxMind's free GeoLite2 database is what most people start with. It comes in two formats:
- GeoLite2-City: Country, region, city, postal code, approximate coordinates
- GeoLite2-Country: Just country-level data (smaller, faster)
The database is a binary .mmdb file you download and query locally — no API calls, no latency, no rate limits.
// Install: npm install maxmind
const maxmind = require('maxmind');
async function lookupIP(ip) {
const lookup = await maxmind.open('/path/to/GeoLite2-City.mmdb');
const result = lookup.get(ip);
if (result) {
return {
country: result.country?.iso_code, // "US"
countryName: result.country?.names?.en, // "United States"
region: result.subdivisions?.[0]?.iso_code, // "CA"
city: result.city?.names?.en, // "San Francisco"
postal: result.postal?.code, // "94102"
latitude: result.location?.latitude, // 37.7749
longitude: result.location?.longitude, // -122.4194
accuracyRadius: result.location?.accuracy_radius, // 20 (km)
timezone: result.location?.time_zone // "America/Los_Angeles"
};
}
return null;
}
Accuracy Reality Check
People overestimate IP geolocation accuracy. Here's the truth:
Country Level: ████████████████████░ ~99% (very reliable)
State Level: ████████████████░░░░░ ~80% (usually right)
City Level: ████████████░░░░░░░░░ ~60% (hit or miss)
ZIP/Postal: ████████░░░░░░░░░░░░░ ~40% (don't rely on this)
Street Level: ██░░░░░░░░░░░░░░░░░░░ ~10% (basically useless)
For geolocking (country-level), IP geolocation is perfectly fine. For geofencing (precise areas), you need GPS or the browser Geolocation API.
What Breaks IP Geolocation
- VPNs: User appears to be wherever the VPN server is
- Proxies/Tor: Similar to VPNs, masks real IP
- Mobile carriers: A user in Dallas might route through a gateway in Houston
- Corporate VPNs: Employee in Tokyo shows up as being in New York HQ
- Satellite internet (Starlink): IP might geolocate to a ground station hundreds of miles away
- CGN (Carrier-Grade NAT): Multiple users share one public IP, which might geolocate to the ISP's facility, not the user
Implementing Geolocking in a Backend
Let's get practical. Here are several approaches, from application-level to infrastructure-level.
Approach 1: Node.js/Express Middleware
The most flexible approach — you control everything in your application code.
// geolock.middleware.js
const maxmind = require('maxmind');
const path = require('path');
let geoLookup = null;
// Initialize the database once at startup
async function initGeoIP() {
geoLookup = await maxmind.open(
path.join(__dirname, '../data/GeoLite2-Country.mmdb')
);
console.log('GeoIP database loaded');
}
// Extract real IP behind proxies/load balancers
function getClientIP(req) {
// Check common proxy headers (in order of trust)
const forwarded = req.headers['x-forwarded-for'];
if (forwarded) {
// x-forwarded-for can be a comma-separated list
// The first IP is the original client
return forwarded.split(',')[0].trim();
}
// Cloudflare
if (req.headers['cf-connecting-ip']) {
return req.headers['cf-connecting-ip'];
}
// AWS ALB / ELB
if (req.headers['x-real-ip']) {
return req.headers['x-real-ip'];
}
return req.socket.remoteAddress;
}
// Middleware factory: pass allowed countries
function geolock(allowedCountries, options = {}) {
const {
blockMessage = 'This content is not available in your region.',
blockStatusCode = 403,
allowUnknown = false, // What to do if we can't determine location
} = options;
return (req, res, next) => {
if (!geoLookup) {
console.error('GeoIP database not initialized');
return next(); // Fail open — or fail closed depending on your policy
}
const ip = getClientIP(req);
const result = geoLookup.get(ip);
const country = result?.country?.iso_code;
// Attach geo info to request for downstream use
req.geo = {
ip,
country,
countryName: result?.country?.names?.en
};
if (!country) {
if (allowUnknown) return next();
return res.status(blockStatusCode).json({
error: blockMessage,
code: 'GEO_UNKNOWN'
});
}
if (!allowedCountries.includes(country)) {
return res.status(blockStatusCode).json({
error: blockMessage,
code: 'GEO_BLOCKED',
detectedCountry: country
});
}
next();
};
}
module.exports = { initGeoIP, geolock, getClientIP };
Using the middleware:
const express = require('express');
const { initGeoIP, geolock } = require('./middleware/geolock');
const app = express();
// Initialize GeoIP before starting the server
initGeoIP().then(() => {
// Block entire API to specific countries
app.use('/api/v1', geolock(['US', 'CA', 'GB', 'DE', 'FR']));
// Or per-route restrictions
app.get('/api/content/us-only',
geolock(['US']),
(req, res) => {
res.json({
content: 'This is US-only content',
detectedCountry: req.geo.country
});
}
);
// Different content per region
app.get('/api/pricing', (req, res) => {
const pricing = getPricingForCountry(req.geo.country);
res.json(pricing);
});
app.listen(3000);
});
Approach 2: Cloudflare Workers (Edge-Level)
If you use Cloudflare, you get geo headers for free. Cloudflare adds them before the request even reaches your origin server.
// Cloudflare Worker — runs at the edge
export default {
async fetch(request) {
// Cloudflare provides these automatically
const country = request.cf?.country; // "US"
const region = request.cf?.region; // "California"
const city = request.cf?.city; // "San Francisco"
const latitude = request.cf?.latitude; // "37.7749"
const longitude = request.cf?.longitude; // "-122.4194"
const timezone = request.cf?.timezone; // "America/Los_Angeles"
const blockedCountries = ['RU', 'CN', 'KP'];
if (blockedCountries.includes(country)) {
return new Response(
JSON.stringify({
error: 'Service not available in your region',
code: 'GEO_BLOCKED'
}),
{
status: 403,
headers: { 'Content-Type': 'application/json' }
}
);
}
// Pass geo info to origin via headers
const modifiedRequest = new Request(request);
modifiedRequest.headers.set('X-Geo-Country', country || 'unknown');
modifiedRequest.headers.set('X-Geo-Region', region || 'unknown');
modifiedRequest.headers.set('X-Geo-City', city || 'unknown');
return fetch(modifiedRequest);
}
};
This is excellent because the blocking happens at the edge — blocked users don't even reach your server, saving you bandwidth and compute.
Approach 3: AWS CloudFront Geo-Restriction
CloudFront has built-in geo-restriction. No code needed — just configuration.
{
"DistributionConfig": {
"Restrictions": {
"GeoRestriction": {
"RestrictionType": "whitelist",
"Quantity": 3,
"Items": ["US", "CA", "GB"]
}
}
}
}
Or using AWS CDK:
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
const distribution = new cloudfront.Distribution(this, 'MyDist', {
defaultBehavior: { origin: myOrigin },
geoRestriction: cloudfront.GeoRestriction.allowlist(
'US', 'CA', 'GB', 'DE', 'FR'
),
});
CloudFront returns a 403 with a generic error page to blocked users. Simple and effective, but it's all-or-nothing — you can't do per-route restrictions or show different content per region.
Approach 4: Nginx Geo Module
If Nginx is your reverse proxy, you can do geo-blocking at that layer:
# /etc/nginx/conf.d/geoblock.conf
# Load the GeoIP2 module (requires ngx_http_geoip2_module)
geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
auto_reload 60m;
$geoip2_data_country_code country iso_code;
}
# Define a map of blocked countries
map $geoip2_data_country_code $blocked_country {
default 0;
RU 1;
CN 1;
KP 1;
}
server {
listen 80;
server_name example.com;
# Block at the server level
if ($blocked_country) {
return 403 '{"error": "Not available in your region"}';
}
# Or pass geo info to your backend
location /api/ {
proxy_pass http://backend;
proxy_set_header X-Geo-Country $geoip2_data_country_code;
}
# Region-specific routing
location /content/ {
if ($geoip2_data_country_code = "US") {
proxy_pass http://us-backend;
}
if ($geoip2_data_country_code = "EU") {
proxy_pass http://eu-backend;
}
proxy_pass http://default-backend;
}
}
Comparison of Approaches
Flexibility Performance Ease of Setup Per-Route Control
----------- ----------- ------------- -----------------
App Middleware ★★★★★ ★★★ ★★★★ ★★★★★
Cloudflare Worker ★★★★ ★★★★★ ★★★★★ ★★★★
AWS CloudFront ★★ ★★★★★ ★★★★★ ★
Nginx GeoIP ★★★ ★★★★ ★★★ ★★★
Implementing Geofencing on the Frontend
For precise geofencing (trigger actions based on entering/exiting a zone), you need the browser Geolocation API or native mobile APIs.
Setting Up a Geofence with the Browser API
class GeofenceManager {
constructor() {
this.fences = [];
this.watchId = null;
this.insideFences = new Set();
}
// Add a circular geofence
addFence({ id, latitude, longitude, radiusMeters, onEnter, onExit }) {
this.fences.push({ id, latitude, longitude, radiusMeters, onEnter, onExit });
}
// Haversine formula — distance between two lat/lng points
getDistanceMeters(lat1, lon1, lat2, lon2) {
const R = 6371000; // Earth's radius in meters
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
// Start watching position
start() {
if (!navigator.geolocation) {
console.error('Geolocation not supported');
return;
}
this.watchId = navigator.geolocation.watchPosition(
(position) => this.checkFences(position),
(error) => console.error('Geolocation error:', error),
{
enableHighAccuracy: true,
maximumAge: 10000, // Accept 10-second-old positions
timeout: 15000
}
);
}
checkFences(position) {
const { latitude, longitude } = position.coords;
for (const fence of this.fences) {
const distance = this.getDistanceMeters(
latitude, longitude,
fence.latitude, fence.longitude
);
const isInside = distance <= fence.radiusMeters;
const wasInside = this.insideFences.has(fence.id);
if (isInside && !wasInside) {
this.insideFences.add(fence.id);
fence.onEnter?.({ fence, distance, position });
} else if (!isInside && wasInside) {
this.insideFences.delete(fence.id);
fence.onExit?.({ fence, distance, position });
}
}
}
stop() {
if (this.watchId !== null) {
navigator.geolocation.clearWatch(this.watchId);
this.watchId = null;
}
}
}
// Usage
const manager = new GeofenceManager();
manager.addFence({
id: 'office',
latitude: 37.7749,
longitude: -122.4194,
radiusMeters: 200,
onEnter: () => console.log('Welcome to the office!'),
onExit: () => console.log('You left the office area.')
});
manager.addFence({
id: 'coffee-shop',
latitude: 37.7755,
longitude: -122.4180,
radiusMeters: 50,
onEnter: ({ distance }) => {
showNotification(`Coffee shop nearby! (${Math.round(distance)}m away)`);
}
});
manager.start();
Handling Permission Denials Gracefully
async function requestLocation() {
// Check permission status first (Permissions API)
if (navigator.permissions) {
const status = await navigator.permissions.query({ name: 'geolocation' });
if (status.state === 'denied') {
// Don't even ask — show a helpful message instead
showUI('location-blocked', {
message: 'Location access was denied. To use this feature, ' +
'enable location in your browser settings.'
});
return null;
}
// Listen for permission changes
status.addEventListener('change', () => {
if (status.state === 'granted') {
// User just enabled location — retry
startGeofencing();
}
});
}
// Now request the position
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 10000
});
});
}
Geofencing with Coordinates: The Math
When your geofence isn't a simple circle, you need more sophisticated algorithms.
The Haversine Formula
For "is this point within X km of that point?" — which covers circular geofences:
┌──────────────────┐
│ Haversine │
│ Formula │
│ │
Point A ──────>│ a = sin²(Δlat/2)│──────> Distance
(lat, lon) │ + cos(lat1) │ in km
│ * cos(lat2) │
Point B ──────>│ * sin²(Δlon/2)│
(lat, lon) │ │
│ d = 2R * atan2( │
│ √a, √(1-a)) │
└──────────────────┘
import math
def haversine(lat1, lon1, lat2, lon2):
"""Returns distance in kilometers between two points."""
R = 6371 # Earth's radius in km
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = (math.sin(dlat / 2) ** 2 +
math.cos(math.radians(lat1)) *
math.cos(math.radians(lat2)) *
math.sin(dlon / 2) ** 2)
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
return R * c
# Example: Distance between New York and London
dist = haversine(40.7128, -74.0060, 51.5074, -0.1278)
print(f"NYC to London: {dist:.0f} km") # ~5570 km
Point-in-Polygon (Ray Casting Algorithm)
For irregular geofence shapes (delivery zones, city boundaries, restricted areas), you need point-in-polygon testing:
/**
* Ray casting algorithm — determines if a point is inside a polygon.
* Shoots a ray from the point to infinity and counts how many times
* it crosses polygon edges. Odd = inside, Even = outside.
*
* @param {number} lat - Point latitude
* @param {number} lon - Point longitude
* @param {Array} polygon - Array of [lat, lon] pairs defining the polygon
* @returns {boolean}
*/
function isPointInPolygon(lat, lon, polygon) {
let inside = false;
const n = polygon.length;
for (let i = 0, j = n - 1; i < n; j = i++) {
const [yi, xi] = polygon[i];
const [yj, xj] = polygon[j];
const intersect = ((yi > lon) !== (yj > lon)) &&
(lat < (xj - xi) * (lon - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
// Define a delivery zone polygon
const deliveryZone = [
[37.78, -122.42],
[37.78, -122.40],
[37.76, -122.40],
[37.76, -122.42],
];
console.log(isPointInPolygon(37.77, -122.41, deliveryZone)); // true
console.log(isPointInPolygon(37.80, -122.41, deliveryZone)); // false
PostGIS for Database-Level Geo Queries
For serious geofencing at scale, do the math in the database with PostGIS:
-- Enable PostGIS extension
CREATE EXTENSION IF NOT EXISTS postgis;
-- Create a table for geofences
CREATE TABLE geofences (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
fence_type VARCHAR(50) NOT NULL, -- 'circle' or 'polygon'
center GEOGRAPHY(POINT, 4326), -- For circular fences
radius_meters FLOAT, -- For circular fences
boundary GEOGRAPHY(POLYGON, 4326) -- For polygon fences
);
-- Insert a circular geofence (500m radius around Times Square)
INSERT INTO geofences (name, fence_type, center, radius_meters)
VALUES (
'Times Square Zone',
'circle',
ST_GeogFromText('POINT(-73.9857 40.7484)'),
500
);
-- Insert a polygon geofence (Manhattan rough boundary)
INSERT INTO geofences (name, fence_type, boundary)
VALUES (
'Manhattan',
'polygon',
ST_GeogFromText('POLYGON((-74.0479 40.6829, -73.9718 40.7061,
-73.9420 40.7748, -73.9299 40.8007, -73.9340 40.8479,
-74.0096 40.7513, -74.0479 40.6829))')
);
-- Check if a point is inside any geofence
SELECT id, name, fence_type
FROM geofences
WHERE
(fence_type = 'circle' AND
ST_DWithin(center, ST_GeogFromText('POINT(-73.9851 40.7580)'), radius_meters))
OR
(fence_type = 'polygon' AND
ST_Within(
ST_GeogFromText('POINT(-73.9851 40.7580)')::geometry,
boundary::geometry
));
-- Find all users within 1km of a point (for proximity alerts)
SELECT user_id, name,
ST_Distance(
location,
ST_GeogFromText('POINT(-73.9857 40.7484)')
) AS distance_meters
FROM users
WHERE ST_DWithin(
location,
ST_GeogFromText('POINT(-73.9857 40.7484)'),
1000 -- 1000 meters
)
ORDER BY distance_meters;
PostGIS uses spatial indexes (R-trees) under the hood, so these queries are fast even with millions of rows.
CDN-Level Geoblocking
The easiest way to implement geo-restriction for most use cases. Let the CDN handle it.
Cloudflare
Cloudflare offers geo-blocking through multiple mechanisms:
- IP Access Rules (dashboard): Block entire countries with a few clicks
- WAF Custom Rules: More granular — block specific countries for specific paths
- Workers (code): Full programmatic control (shown earlier)
Example WAF rule (Cloudflare dashboard expression):
(ip.geoip.country in {"RU" "CN" "KP"}) and (http.request.uri.path contains "/api/")
AWS CloudFront + Lambda@Edge
For more complex logic than CloudFront's built-in geo-restriction:
// Lambda@Edge function (viewer request)
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
// CloudFront adds this header automatically
const country = headers['cloudfront-viewer-country']?.[0]?.value;
const allowedCountries = ['US', 'CA', 'GB', 'DE', 'FR', 'AU'];
if (country && !allowedCountries.includes(country)) {
return {
status: '403',
statusDescription: 'Forbidden',
headers: {
'content-type': [{ value: 'application/json' }],
'cache-control': [{ value: 'no-store' }]
},
body: JSON.stringify({
error: 'This service is not available in your region.',
country: country
})
};
}
// Add geo header for the origin to use
request.headers['x-viewer-country'] = [{ value: country || 'unknown' }];
return request;
};
Akamai
Akamai provides EdgeScape data that includes geographic information. You can use it in property configurations or EdgeWorkers:
// Akamai EdgeWorker
import { httpRequest } from 'http-request';
export async function onClientRequest(request) {
const country = request.getVariable('PMUSER_COUNTRY');
if (['RU', 'CN', 'KP'].includes(country)) {
request.respondWith(403, {},
JSON.stringify({ error: 'Not available in your region' })
);
}
}
VPN/Proxy Detection and Bypass Prevention
Geo-restrictions are only as good as your ability to detect circumvention.
The Problem
User in China --> VPN Server in US --> Your Server
|
Sees IP from US
Thinks user is in US
Allows access ❌
Detection Strategies
1. VPN/Proxy IP Databases
Services maintain lists of known VPN, proxy, and Tor exit node IPs:
const maxmind = require('maxmind');
async function checkVPN(ip) {
// MaxMind GeoIP2 Anonymous IP database (paid)
const anonLookup = await maxmind.open('/path/to/GeoIP2-Anonymous-IP.mmdb');
const result = anonLookup.get(ip);
return {
isVPN: result?.is_anonymous_vpn || false,
isProxy: result?.is_public_proxy || false,
isTorExitNode: result?.is_tor_exit_node || false,
isHostingProvider: result?.is_hosting_provider || false,
isResidentialProxy: result?.is_residential_proxy || false
};
}
2. DNS Leak Detection
Even with a VPN, DNS requests sometimes leak to the user's real ISP:
// Client-side: Force a DNS lookup to your controlled domain
// and check if the resolver IP matches the expected VPN location
async function checkDNSLeak() {
const uniqueId = crypto.randomUUID();
// Request a unique subdomain — the DNS query reveals the resolver
const response = await fetch(`https://${uniqueId}.dns-check.yourservice.com/check`);
const data = await response.json();
// Server-side: log which DNS resolver made the query for this uniqueId
// If the resolver is in a different country than the client IP, flag it
return data;
}
3. Timezone / Language Mismatch
A user connecting from a "US" IP but with Accept-Language: zh-CN and a timezone offset of +8 is suspicious:
function detectMismatch(req) {
const geoCountry = req.geo.country; // From IP lookup
const language = req.headers['accept-language']?.split(',')[0];
// Client sends timezone via JS
const clientTimezone = req.body.timezone; // e.g., "Asia/Shanghai"
const suspiciousSignals = [];
if (geoCountry === 'US' && language?.startsWith('zh')) {
suspiciousSignals.push('language_mismatch');
}
if (geoCountry === 'US' && clientTimezone?.startsWith('Asia/')) {
suspiciousSignals.push('timezone_mismatch');
}
return {
suspicious: suspiciousSignals.length > 0,
signals: suspiciousSignals,
confidence: suspiciousSignals.length / 2 // rough score
};
}
4. WebRTC IP Leak (Browser)
WebRTC can reveal a user's real IP even behind a VPN:
// This is used by streaming services to detect VPNs
// Note: Modern browsers are patching this, and most VPN extensions block it
async function getWebRTCIPs() {
return new Promise((resolve) => {
const ips = new Set();
const pc = new RTCPeerConnection({ iceServers: [] });
pc.createDataChannel('');
pc.createOffer().then(offer => pc.setLocalDescription(offer));
pc.onicecandidate = (event) => {
if (!event.candidate) {
resolve([...ips]);
return;
}
const parts = event.candidate.candidate.split(' ');
const ip = parts[4];
if (ip && !ip.includes(':')) { // Skip IPv6
ips.add(ip);
}
};
setTimeout(() => resolve([...ips]), 3000);
});
}
Reality Check on VPN Detection
No VPN detection is perfect. Here's how effective each method is:
| Method | Detection Rate | False Positives | Cost |
|---|---|---|---|
| VPN IP databases | ~70-85% | Low | Paid (MaxMind, IPQualityScore) |
| DNS leak detection | ~20-30% | Very low | Custom infrastructure |
| Timezone mismatch | ~40-50% | Medium (travelers) | Free |
| WebRTC leak | ~30-40% (declining) | Low | Free |
| Combined approach | ~85-95% | Medium | Moderate |
Streaming services like Netflix invest heavily in VPN detection and still can't catch everything. For most applications, a VPN IP database plus timezone checks is good enough.
Legal Considerations
Geo-restriction isn't just a technical decision — there are legal implications.
GDPR (EU)
- If you geoblock EU users, you might not need to comply with GDPR for those users
- But if even one EU resident accesses your service, you could be subject to it
- The EU has regulations against unjustified geoblocking within the EU (Regulation 2018/302) — you can't block French users from buying from a German e-commerce site without justification
CCPA (California)
- Similar to GDPR but for California residents
- Geoblocking California doesn't exempt you if Californians can still access your service
Data Localization Laws
Some countries require that their citizens' data be stored within the country:
| Country | Law | Requirement |
|---|---|---|
| Russia | Federal Law No. 242-FZ | Personal data of Russian citizens must be stored in Russia |
| China | PIPL + CSL | Certain data must remain in China, cross-border transfers need approval |
| India | DPDPA 2023 | Government can restrict data transfers to specific countries |
| Brazil | LGPD | Less strict, but cross-border transfers need adequate protection |
| EU | GDPR | Data can leave EU only to "adequate" countries or with safeguards |
Key Takeaway
Geoblocking can help with compliance, but it's not a silver bullet. Consult a lawyer for your specific situation. Seriously.
How the Big Players Handle Geo-Restrictions
Netflix
Netflix operates different content libraries in every country due to licensing agreements.
Their approach:
- IP geolocation at the edge (likely custom + commercial databases)
- Aggressive VPN detection (they maintain their own database of VPN/proxy IPs)
- DNS-based detection (blocks known Smart DNS services)
- Residential proxy detection (newer — catches the latest evasion tech)
- Account region locking (your account has a "home" country)
Netflix's system is so thorough that VPN providers actively market "works with Netflix" as a feature — and many fail at it.
Spotify
- Uses IP geolocation for initial region detection
- Requires periodic "home location" verification
- If you travel, you get a grace period before content changes
- Less aggressive VPN detection than Netflix (music licensing is simpler)
Disney+
- Similar to Netflix but launched with stricter geoblocking
- Uses a combination of IP geolocation and payment method country
- Your billing country must match your viewing country
YouTube
- Some content is geo-restricted by uploaders
- YouTube Premium pricing varies by region
- Uses IP geolocation — relatively easy to bypass compared to Netflix
Common Mistakes and Edge Cases
Mistake 1: Trusting X-Forwarded-For Blindly
// DANGEROUS — any client can set this header
const ip = req.headers['x-forwarded-for'];
// BETTER — only trust it if your proxy/load balancer sets it
// And take the LAST IP added by YOUR infrastructure, not the first
function getTrustedIP(req) {
const xff = req.headers['x-forwarded-for'];
if (!xff) return req.socket.remoteAddress;
const ips = xff.split(',').map(ip => ip.trim());
// If you have 1 trusted proxy, the real client IP is second from right
// Adjust based on your proxy chain length
return ips[ips.length - 1]; // Or ips[0] if you trust all proxies
}
Mistake 2: Failing Closed Without a Fallback
If your GeoIP database fails to load or returns null, what happens? If you block all unknown traffic, a database corruption takes down your entire service.
// Have a fallback strategy
function getGeoDecision(ip) {
try {
const result = geoLookup.get(ip);
if (!result) return { action: 'allow', reason: 'unknown_ip' }; // Fail open
return { action: isAllowed(result) ? 'allow' : 'block', country: result.country };
} catch (error) {
logger.error('GeoIP lookup failed', { ip, error });
return { action: 'allow', reason: 'lookup_failed' }; // Fail open
}
}
Mistake 3: Not Updating the GeoIP Database
IP allocations change constantly. If you downloaded the GeoLite2 database a year ago and never updated it, your accuracy has degraded.
# Set up a cron job to update weekly
# (MaxMind requires a license key, even for the free database)
0 3 * * 3 /usr/local/bin/geoipupdate -v
Mistake 4: Caching Geo Decisions Too Aggressively
User with IP 1.2.3.4 --> CDN caches page with "US content"
Different user with IP 5.6.7.8 --> Gets cached US content (wrong!)
Always use Vary: X-Geo-Country or equivalent when caching geo-specific responses.
Mistake 5: Forgetting IPv6
Many GeoIP implementations only handle IPv4. Modern traffic increasingly uses IPv6, and if you can't geolocate it, your blocking has a giant hole.
Mistake 6: Not Handling Travelers
A legitimate US user traveling in Japan shouldn't be permanently locked out. Consider:
- Grace periods (Spotify gives you 14 days)
- Account-based region (use signup country, not current IP)
- Allow users to confirm their home country
Architecture Patterns for Geo-Aware Applications
Pattern 1: Edge + Origin (Recommended for Most)
┌────────────────────────────────────────────────────────┐
│ CDN / Edge Layer │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ CF Worker │ │ CF Worker │ │ CF Worker │ ... │
│ │ (US Edge) │ │ (EU Edge) │ │ (AP Edge) │ │
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│ │ Adds geo │ Blocks │ Adds geo │
│ │ headers │ if needed │ headers │
└────────┼───────────────┼────────────────┼──────────────┘
│ │ │
v v v
┌────────────────────────────────────────────────────────┐
│ Origin Server(s) │
│ │
│ Reads X-Geo-Country header │
│ Serves appropriate content/pricing │
│ Handles edge cases (VPN detection, etc.) │
└────────────────────────────────────────────────────────┘
The edge layer does coarse geo-blocking (block banned countries) and adds geo headers. The origin server uses those headers for fine-grained logic (pricing, content selection).
Pattern 2: Multi-Region with Geo-Routing
DNS (GeoDNS / Route 53)
│
┌────────────┼────────────┐
│ │ │
v v v
┌────────┐ ┌────────┐ ┌────────┐
│ US-East │ │ EU-West│ │ AP-SE │
│ Region │ │ Region │ │ Region │
├────────┤ ├────────┤ ├────────┤
│ App │ │ App │ │ App │
│ DB │ │ DB │ │ DB │
│ Cache │ │ Cache │ │ Cache │
└────────┘ └────────┘ └────────┘
For data residency compliance: users in the EU are routed to EU servers with EU-only databases. No data leaves the region.
Pattern 3: Hybrid — Server Decides, Client Refines
1. User opens app
2. Server checks IP --> Country = US --> Serves US content
3. Client requests GPS permission
4. User grants --> Client gets precise coordinates
5. Client sends coordinates to server
6. Server checks point-in-polygon --> User is in delivery zone
7. Server enables local delivery features
This combines IP geolocation (fast, no permission needed) with GPS (precise, permission required) for the best of both worlds.
Quick Decision Guide
Trying to figure out which approach to use? Here's a flowchart:
What do you need?
│
├── Block entire countries from accessing your service?
│ └── CDN-level geoblocking (Cloudflare, CloudFront)
│ Easiest, fastest, cheapest
│
├── Show different content/pricing per country?
│ └── Edge layer (Cloudflare Worker) + origin logic
│ Geo headers at edge, business logic at origin
│
├── Comply with data residency laws?
│ └── Multi-region architecture with GeoDNS
│ Data stays in region, users routed to nearest
│
├── Trigger actions when users enter/exit areas?
│ └── Browser Geolocation API + Haversine/point-in-polygon
│ Requires user permission, GPS-level accuracy
│
└── Complex geo queries on large datasets?
└── PostGIS with spatial indexes
Millions of points/polygons, fast queries
Wrapping Up
Geofencing and geolocking are foundational capabilities for any application that serves users globally. The key takeaways:
- IP geolocation is good enough for country-level restrictions — use MaxMind GeoLite2 or your CDN's built-in geo headers.
- CDN-level blocking is the easiest first step — Cloudflare and CloudFront can block countries with zero code.
- For precise geofencing, you need GPS — which means user permission and client-side code.
- VPN detection is an arms race — use a commercial IP reputation database if it matters to your business.
- Don't forget the legal side — geoblocking has compliance implications in both directions.
- Always have a fallback — GeoIP databases can fail, IPs can be unresolvable, and travelers exist.
Start simple (CDN-level blocking or a basic middleware), and add complexity only when your requirements demand it.
If you found this guide helpful and want to stay updated on more engineering deep-dives, connect with me on LinkedIn. I regularly share insights on backend development, system design, and building for scale. Let's connect!
Top comments (0)