How to Detect VPN Users in 2026: A Developer's Guide (JavaScript + Server)
Code-first guide to detecting VPN, proxy, Tor and datacenter IPs. JS signals (WebRTC, timezone, language) + server signals (ASN, JA3, RTT) and how to combine.
This is a practical, code-first guide to detecting VPN, proxy, Tor, and datacenter IPs on your own website. No hand-waving, no marketing. We cover client-side JavaScript signals you can collect in the browser, server-side signals you extract from the TCP and TLS stacks, and how to combine them without frustrating real users. All example code is MIT-permissible — copy it.
Client-side signals to collect
1. Timezone vs claimed IP
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Send tz to your backend, which compares against
// the geo timezone of the request's IP.A browser reporting America/New_York from an IP geolocated to Frankfurt is a strong VPN tell. False-positive rate is below 1% in practice — legitimate reasons exist (traveling users), but they are rare.
2. Language preference
const lang = navigator.language;
const langs = navigator.languages;
// "en-US", ["en-US","en","fr"]
// Compare against the expected language for the IP's country.Lower precision than timezone but useful as a corroborating signal. Users in Taiwan reporting en-US are uninteresting; users in mainland China reporting en-US on a Shanghai IP are mildly suspicious.
3. WebRTC leak detection
async function tryWebRTCIP() {
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
pc.createDataChannel("probe");
const seen = new Set();
pc.onicecandidate = (e) => {
if (!e.candidate) return;
const [, , , , ip, , typeKw, type] = e.candidate.candidate.split(" ");
if (typeKw === "typ" && type === "srflx" && ip) seen.add(ip);
};
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
await new Promise((r) => setTimeout(r, 1500));
pc.close();
return [...seen][0];
}
// If the returned IP differs from the HTTP-layer source IP,
// the browser is behind a VPN that does not tunnel UDP.WebRTC leaks are among the highest-signal VPN tells available to the client side because they reveal the real public IP that STUN servers observe. Modern browsers have multiple mitigations (Firefox strict mode, Brave shields, Chrome WebRTC IP handling policy) so the leak is not guaranteed, but when it fires it is near-definitive.
4. Client-measured RTT (SNITCH)
// After a fetch completes, pull the timing entry
// and extract the TCP and TLS handshake RTTs.
const entries = performance.getEntriesByName(url);
const e = entries[entries.length - 1];
const tcpRttMs = e.secureConnectionStart - e.connectStart;
const tlsRttMs = e.connectEnd - e.secureConnectionStart;On a direct connection, TCP and TLS RTT are close to equal (they traverse the same path). On a VPN connection, the TLS handshake has to traverse the tunnel while TCP only reaches the ingress, producing a detectable differential — the basis of the SNITCH paper (NDSS 2025).
Server-side signals to collect
5. ASN / org classification
Every IP belongs to an Autonomous System. Datacenter ASNs (AWS AS16509, Google AS15169, Microsoft AS8075, Hetzner AS24940, OVH AS16276, DigitalOcean AS14061) are a strong signal that the connection is not a consumer home broadband. Use MaxMind GeoLite2 ASN (free) or ip-api.com (free fallback) for lookups.
6. JA3 / JA4 TLS fingerprint
In Go with the dreadl0ck/ja3 package, you can extract the JA3 hash from the TLS ClientHello at the moment of the handshake. Match against a known-VPN- fingerprint database. See our JA3/JA4 deep dive for a full explainer.
7. Tor exit list
// Refresh hourly
fetch("https://check.torproject.org/exit-addresses")
.then(r => r.text())
.then(t => {
const exits = new Set();
for (const line of t.split("\n")) {
if (line.startsWith("ExitAddress ")) {
exits.add(line.split(" ")[1]);
}
}
// store in your fast lookup cache
});Combining the signals
Assign each signal a weight. Sum the weights of signals that match. Produce a verdict based on thresholds:
| Score | Verdict | Recommended action |
|---|---|---|
| ≥ 0.75 | vpn_detected | Block or force step-up auth |
| 0.50–0.75 | vpn_likely | Soft friction (captcha, email verify) |
| 0.30–0.50 | suspicious | Log and monitor |
| < 0.30 | clean | Normal flow |
The shortcut: use IPLogs
You can implement the pipeline above yourself, or you can POST one JSON payload to the IPLogs API and get back the full verdict, score, confidence, and signal set — no signup, no API key, free for reasonable use.
curl -X POST https://iplogs.com/v1/check \
-H 'content-type: application/json' \
-d '{"ip":"45.82.245.81"}'See the full API reference for signal catalog, request fields, and client examples in Python, Node.js, Go, PHP, and Ruby.
Check any IP against the 7-layer pipeline
The detection methods described above are all available through the IPLogs public API, free, no signup required.
Try the IP checker →