How to Block VPN Users in Node.js

Detect and block VPN, proxy and Tor users in Node.js with an Express middleware, caching and a safe fallback.

1. Get an API key

Create a free account and copy your API key from the dashboard. The Free plan includes 10,000 lookups a month. Keep the key on your server in an environment variable called VPNBLOCKER_KEY, never in code that runs in the browser.

2. Look up the IP

Put the helpers in their own file, vpnblocker.js. This uses the built-in fetch in Node.js 18 and later, with a 3 second timeout. If the lookup fails it returns null, so you can let the user through.

export async function lookup(ip) {
  try {
    const res = await fetch(`https://api.vpnblocker.net/v2/json/${encodeURIComponent(ip)}`, {
      headers: { 'X-API-KEY': process.env.VPNBLOCKER_KEY },
      signal: AbortSignal.timeout(3000),
    });
    return res.ok ? await res.json() : null;
  } catch {
    return null; // network error or timeout
  }
}

3. Cache the answer

Caching saves lookups and keeps responses fast. An hour is a good length, because Tor exits change every hour.

const cache = new Map(); // ip -> { r, expires }

export async function cachedLookup(ip) {
  const hit = cache.get(ip);
  if (hit && hit.expires > Date.now()) return hit.r;
  const r = await lookup(ip);
  if (r) cache.set(ip, { r, expires: Date.now() + 60 * 60 * 1000 });
  return r;
}

A Map grows forever and isn’t shared between processes. In production, use a cache with a size limit, such as the lru-cache package, or Redis.

4. Decide: block, challenge or allow

This turns a lookup into one of three actions. It’s the same logic as the example in the docs: change the settings at the top to fit your site.

// Tune these to fit your site.
const BLOCK_CATEGORIES = ['vpn', 'proxy', 'tor']; // add 'hosting' to block data centers too
const BLOCK_AT = 90;       // block these categories at or above this confidence
const CHALLENGE_AT = 70;   // challenge anything flagged at or above this confidence
const CLEAN_TRUST_AT = 80; // clean answers below this are little-known addresses

export function decide(r, sensitive = false) {
  const flagged = r['host-ip'] === true || r.categories.length > 0;
  const blockable = r.categories.some((c) => BLOCK_CATEGORIES.includes(c));

  if (blockable && r.confidence >= BLOCK_AT) return 'block';
  if (flagged && r.confidence >= CHALLENGE_AT) return 'challenge';
  if (sensitive && !flagged && r.confidence < CLEAN_TRUST_AT) return 'challenge';
  return 'allow';
}

5. Add an Express middleware

Get the visitor’s real IP from req.ip. If your app runs behind a load balancer or reverse proxy, tell Express with trust proxy. Node.js can report IPv4 addresses as ::ffff:1.2.3.4, so strip that prefix.

import express from 'express';
import { cachedLookup, decide } from './vpnblocker.js';

const app = express();
app.set('trust proxy', 1); // only if you're behind exactly one proxy or load balancer

function vpnCheck({ sensitive = false } = {}) {
  return async (req, res, next) => {
    const ip = req.ip.replace(/^::ffff:/, '');
    const r = await cachedLookup(ip);
    const action = r ? decide(r, sensitive) : 'allow'; // allow if the lookup failed
    if (action === 'block') {
      return res.status(403).send('Please turn off your VPN, proxy or Tor and try again.');
    }
    res.locals.vpnAction = action; // show a captcha when this is 'challenge'
    next();
  };
}

app.post('/signup', vpnCheck(), signupHandler);
app.post('/checkout', vpnCheck({ sensitive: true }), checkoutHandler);

Only trust a forwarded header if every request really reaches your server through that proxy. Otherwise anyone can send a fake one and pretend to be any IP address.

Behind Cloudflare, read the CF-Connecting-IP header instead of req.ip, but only if all traffic reaches your server through Cloudflare.

Block VPNs in your Node.js app today

Create a free account and make your first lookup in minutes. 10,000 lookups a month at no charge.