# How to block Tor exit nodes: seven in ten Tor relays never exit

Tor's exit list held 1,209 addresses on October 17, 2026. Which Tor list to block, how often to refresh it, and the IPv6 exits it leaves out.

Tor's [exit list](https://check.torproject.org/torbulkexitlist) held 1,209 addresses at 04:20 UTC on October 17, 2026, all of them IPv4. The relays in Tor's consensus that morning ran on 9,616, and 6,287 of those 8,940 relays never exit, so a blocklist of every relay is mostly addresses that never send a website a Tor user's request. The exit list misses the other way: when we fetched a page from a server with both IPv4 and IPv6 addresses through Tor, with Tor Browser's settings, 76 of 80 visits arrived over IPv6, from addresses the list doesn't carry.

## Exits and relays

Tor publishes two lists. The exit list is what Tor's own [exit scanner](https://gitlab.torproject.org/api/v4/projects/tpo%2Fnetwork-health%2Fmetrics%2Fexit-scanner/repository/files/src%2Fexitscan.py/raw?ref=main) sees: about once an hour it connects through the exits and records the address each connection arrives from, and it keeps an exit on the list for 48 hours after it was last seen. The consensus lists every relay in any role, guard, middle or exit, with the address it advertises, and [Onionoo](https://metrics.torproject.org/onionoo.html) serves it as JSON.

At 03:00 UTC on October 17, 8,940 relays were running. 2,653 of them were on the exit list. The other 6,287, 70%, were guard and middle relays, which pass traffic between relays and never connect to a site. 7,532 of the consensus's 9,616 addresses ran no relay that exits: block them and you block the people who run Tor relays, and no Tor traffic.

For IPv4, the exit list also records the address actually used rather than the one advertised. 4 of its 1,209 addresses weren't any relay's advertised address, `23.151.8.10` among them.

Our [Tor IP](https://vpndetection.io/databases/tor-ip) database, and the API's `is_tor`, flag any Tor node, relays as well as exits. A match says the address is a Tor node, not that the request came through Tor, which is why the code at the end asks a Tor visitor to confirm their email at signup instead of turning them away.

## Tor's exit list has no IPv6

Tor Browser asks exits for IPv6 first. It starts tor with `IPv6Traffic PreferIPv6` on its SOCKS port ([its default settings](https://gitlab.torproject.org/api/v4/projects/472/repository/files/browser%2Fapp%2Fprofile%2F000-tor-browser.js/raw?ref=tor-browser-153.5.0esr-16.0-1)), and in [Tor's manual](https://manpages.debian.org/testing/tor/tor.1.en.html) the second flag tells exits to connect over IPv6 when a host has both. 2,328 of the 2,659 exit relays running that morning exit over IPv6.

We ran tor 0.4.9.14 with Tor Browser's SOCKS flags and fetched `icanhazip.com`, which has both kinds of address, 80 times on 80 new circuits, from 12:21 to 12:27 UTC on October 18. 73 fetches came back, and 7 timed out after 25 seconds:

| Arrived over | Visits | On Tor's exit list | An exit's advertised address | `is_tor` from our API |
|---|---:|---:|---:|---:|
| IPv4 | 4 | 4 | 4 | 4 |
| IPv6 | 69 | 0 | 65 | 69 |

The 4 IPv6 visits no advertised address matched came from one address, `2a0b:f4c2:1::1`, in a /64 where 60 exits advertise theirs. Our API answered `is_tor` for it too.

So on a site with an IPv6 address, a block on Tor's exit list alone stopped 4 of our 73 visits. The exits' IPv6 addresses from Onionoo, which the nginx script below adds, stopped 65 more.

## How fast the exit list changes

![Line chart of addresses on Tor's exit list each day, September 16 to October 16, 2026: 1,355 on September 16 and 1,219 on October 16, with the biggest drop on October 7. Of September 16's addresses, 1,122 were still listed on October 16.](/blog/media/block-tor-exit-nodes-exit-list.2x.png)

Tor's archive of the list, [CollecTor](https://collector.torproject.org/archive/exit-lists/), keeps every hourly copy. Day by day from September 16 to October 16, 2026:

| | Addresses |
|---|---:|
| On the list each day | 1,209 to 1,424 |
| Different addresses over the 31 days | 1,751 |
| Of September 16's 1,355, still listed a day later | 1,347 (99.4%) |
| A week later | 1,313 (96.9%) |
| Two weeks later | 1,288 (95.1%) |
| 30 days later | 1,122 (82.8%) |
| New a day, mean (max) | 15.2 (41) |
| Gone a day, mean (max) | 19.8 (131) |
| Listed on one day only | 7 |
| On October 16's list but not September 16's | 97 (8.0%) |

Of the 233 September 16 exits gone by October 16, 115 went in two days. Tor's count of relays with the Exit flag fell from 3,013 on October 4 to 2,715 on October 5 ([Tor Metrics](https://metrics.torproject.org/relayflags.html)), and the list, which holds an exit for 48 hours, dropped 112 addresses on October 7. Averaged over every start day in the window, 8.9% of a week-old list had left it.

Tor rebuilds the list about once an hour, so fetch it hourly. More often gains nothing, and a daily fetch leaves out up to a day of new exits: 15 on an average day, 41 on the busiest. The 48 hours it keeps make it err toward blocking: 33 of the morning's 1,209 belonged only to relays that had already left the consensus.

## Block Tor's exits in nginx

This script writes Tor's exits as nginx `deny` rules: the exit list for IPv4, and from Onionoo the advertised IPv6 address of every running exit that exits over IPv6. It needs `curl` and `jq`:

```sh
#!/bin/sh
# Tor's exits, IPv4 and IPv6, as nginx deny rules. Run it hourly from cron.
set -eu
cd /etc/nginx
onionoo='https://onionoo.torproject.org/details?flag=Exit&running=true'
curl -sf -o tor-v4.txt https://check.torproject.org/torbulkexitlist
curl -sf -o tor-v6.json "$onionoo&fields=or_addresses,exit_policy_v6_summary"
jq -r '.relays[] | select(.exit_policy_v6_summary) | .or_addresses[]
    | select(startswith("[")) | ltrimstr("[") | split("]")[0]' tor-v6.json |
    cat tor-v4.txt - | sort -u | sed 's/.*/deny &;/' > tor-exits.conf.new
[ -s tor-v4.txt ] && mv tor-exits.conf.new tor-exits.conf
nginx -t -q && nginx -s reload
```

Run it once, then include its output where Tor shouldn't get through:

```nginx
location /signup {
    include /etc/nginx/tor-exits.conf;
    proxy_pass http://127.0.0.1:3000;
}
```

On nginx 1.29.1 at 12:27 UTC on October 18, the script wrote 2,061 rules, 851 of them IPv6. We sent `/signup` a request from a Tor exit on the list, one from an exit's IPv6 address and one from a home broadband line, which we don't print, each from its real address:

```
190.120.229.98       403
2a03:e600:100::1     403
home line            200
```

Don't put a `return` in that location. nginx runs it before its access checks, so it answers every visitor: a denied address got `200` from a `return` and `403` from a `proxy_pass`.

## Or check each request, and challenge instead of blocking

A block also turns away Tor's ordinary users. [Tor Metrics](https://metrics.torproject.org/userstats-relay-country.html) estimated 3.2 to 4.0 million directly connecting users a day from October 12 to 15, 2026, and about 224,000 more through bridges, the unlisted relays people use where Tor itself is blocked. Tor's [advice to sites that want to block it](https://support.torproject.org/abuse/ban-tor/) starts with "application-level decisions to separate the legitimate users from the jerks", such as letting only registered users post. A [2020 Georgetown study](https://seclab.cs.georgetown.edu/papers/zhang-ephemeral-exits.pdf) found about 8% of the Alexa top 10,000 sites blocked Tor or served Tor users different content.

So decide per route. With our [Express middleware](https://docs.vpndetection.io/integrations/express), a Tor visitor can read, confirms their email at signup, and can't take a payout:

```js
import express from 'express';
import { vpndetection } from 'vpndetection-express';

const app = express();
app.set('trust proxy', 'loopback');
app.use(vpndetection({ apiKey: process.env.VPNDETECTION_API_KEY }));

// What a Tor visitor gets on each route. Everyone else is allowed.
const onTor = { '/signup': 'challenge', '/payout': 'block' };

app.use((req, res, next) => {
    const isTor = req.vpndetection.result?.isTor === true;
    const action = isTor ? (onTor[req.path] ?? 'allow') : 'allow';
    if (action === 'block') {
        return res.status(403).json({ error: 'Not available over Tor.' });
    }
    req.confirmEmail = action === 'challenge';
    next();
});

app.get('/', (req, res) => res.json({ ok: true }));
app.post('/signup', (req, res) => {
    res.json({ ok: true, confirmEmail: req.confirmEmail });
});
app.post('/payout', (req, res) => res.json({ ok: true }));

app.listen(3000);
```

We ran it on Node.js 22.22.2 with Express 5.3.0 and `vpndetection-express` 2.0.12 at 05:33 UTC on October 17, and sent it the two exits from the nginx test:

```bash
for ip in 190.120.229.98 2a03:e600:100::1; do
    for path in signup payout; do
        printf '%-17s %-7s ' "$ip" "$path"
        curl -s -X POST "localhost:3000/$path" \
            -H "X-Forwarded-For: $ip" -w ' %{http_code}\n'
    done
done
```

```
190.120.229.98    signup  {"ok":true,"confirmEmail":true} 200
190.120.229.98    payout  {"error":"Not available over Tor."} 403
2a03:e600:100::1  signup  {"ok":true,"confirmEmail":true} 200
2a03:e600:100::1  payout  {"error":"Not available over Tor."} 403
```

A guard relay whose exit policy rejects everything got the same two answers, since it's a Tor node too. A Comcast home line got `"confirmEmail":false` and its payout. All of them could load `/`. We don't print either address.

`isTor` comes with every key, a free one included ([pricing](https://vpndetection.io/pricing)). With no key, the same app let the Tor exit through on both routes, and a failed lookup did the same, since `result` is then undefined. Set `trust proxy` to your own proxy, as our [Express guide](https://vpndetection.io/blog/detect-vpn-users-express-js) explains. The [API](https://vpndetection.io/api) answers `is_tor` for one address or a batch, and its [docs](https://docs.vpndetection.io) describe the `tor` detail. [Tor IP](https://vpndetection.io/databases/tor-ip) is the same data as a daily download.

