About 4 years ago, I looked into how effective my SSH tarpits were.
Since then, I've also deployed an AI crawler tarpit (technically a maze - it generates an endless number of pages) to serve "content" up to crawlers who ignore my "No Entry" signs.
I thought that it might be interesting to take a quick look at trends for both, so this post provides high-level analysis of the statistics that my tarpits generate.
SSH Tarpit
The analysis that I performed in 2022 used a week of data, so to keep things consistent, I'll do the same here.
from(bucket: "telegraf/telegraf_low_granularity")
|> range(start: 2026-08-01T00:00:00Z, stop: 2026-08-08T00:00:00Z)
|> filter(fn: (r) => r._measurement == "ssh_tarpit")
|> filter(fn: (r) => r._field == "after_sum")
|> filter(fn: (r) => exists r.country)
|> group()
|> sum()
In the last week, my tarpits have burnt 11.6 days of bot time. In 2022, a single week burnt 20.14 days, meaning that 2026 represents a 42.5% reduction in bot time wasted by the tarpit.
Is that because fewer bots get stuck in the tarpit, or because they spend less time in there?
from(bucket: "telegraf/telegraf_low_granularity")
|> range(start: 2026-08-01T00:00:00Z, stop: 2026-08-08T00:00:00Z)
|> filter(fn: (r) => r["_measurement"] == "ssh_tarpit")
|> filter(fn: (r) => r["_field"] == "after_count")
|> aggregateWindow(every: v.windowPeriod, fn: sum, createEmpty: false)
|> group(columns: ["country"])
|> sum()
|> group()
|> sum()
There were 9387 tarpitted connections, vs 6354 in 2022. So we saw 47% more bots, but they stuck around for much shorter times.
Breaking that down by country:
from(bucket: "telegraf/telegraf_low_granularity")
|> range(start: 2026-08-01T00:00:00Z, stop: 2026-08-08T00:00:00Z)
|> filter(fn: (r) => r["_measurement"] == "ssh_tarpit")
|> filter(fn: (r) => r["_field"] == "after_count")
|> filter(fn: (r) => exists r.country)
|> aggregateWindow(every: v.windowPeriod, fn: sum, createEmpty: false)
|> group(columns: ["country"])
|> sum()
|> group()
|> top(n: 5)
| Position | Country | Count |
|---|---|---|
| 1 | Unknown | 3707 |
| 2 | CN | 1550 |
| 3 | IN | 967 |
| 4 | US | 939 |
| 5 | VN | 710 |
This is quite a big change: back in 2022, the US was the most common source of connections (with Russia a close second).
If we look at what proportion of each country's connections stick for over 1 minute:
threshold = 60.0
from(bucket: "telegraf/telegraf_low_granularity")
|> range(start: 2026-08-01T00:00:00Z, stop: 2026-08-08T00:00:00Z)
|> filter(fn: (r) => r._measurement == "ssh_tarpit")
|> filter(fn: (r) => r._field == "after_mean")
|> filter(fn: (r) => exists r.country)
|> group(columns: ["country"])
|> filter(fn: (r) => r._value >= threshold)
|> count()
|> map(fn: (r) => ({ r with
// What percentage of conns?
_value: (float(v: r._value) / 6365.0) * 100.0
}))
|> group()
|> top(n: 5)
We can see the that top 5 countries by stick time have shifted:
| Position | 2026 | 2022 | 2021 | |||
|---|---|---|---|---|---|---|
| 1 | VN | 8.3% | CN | 4.95% | CN | 10.64% |
| 2 | Unknown | 8.13% | VN | 1.29% | VN | 1.29% |
| 3 | CN | 0.88% | RU | 0.39% | RU | 0.39% |
| 4 | IR | 0.29% | US | 0.17% | US | 0.17% |
| 5 | US | 0.16% | ZA | 0.09% | ZA | 0.09% |
Connections originating from China are now much less likely to stick than previously.
We can expect, then, that the average and max stick times have also shifted quite heavily:
from(bucket: "telegraf/telegraf_low_granularity")
|> range(start: 2026-08-01T00:00:00Z, stop: 2026-08-08T00:00:00Z)
|> filter(fn: (r) => r._measurement == "ssh_tarpit")
|> filter(fn: (r) => r._field == "after_mean")
|> aggregateWindow(every: 1d, fn: mean)
|> group()
|> mean()
from(bucket: "telegraf/telegraf_low_granularity")
|> range(start: 2026-08-01T00:00:00Z, stop: 2026-08-08T00:00:00Z)
|> filter(fn: (r) => r._measurement == "ssh_tarpit")
|> filter(fn: (r) => r._field == "after_max")
|> aggregateWindow(every: 1d, fn: max)
|> group()
|> max()
| Stat | 2022 | 2026 | Delta |
|---|---|---|---|
| Mean | 539.31s | 16.50s | - 97% |
| Max | 273670s | 582828s | + 112% |
So, despite the maximum stick time increasing significantly, the mean stick time greatly reduced in 2026. For the record, the biggest stick time (7 days) was a bot from Sweden.
Summary
Four years later, running a SSH tarpit looks like this:
| Stats | %age change |
|---|---|
| Bots entering | + 47% |
| Total Time Wasted | - 42.5% |
| Average Stick Time | - 97% |
The tarpit is still effective enough to give some warm fuzzies but also probably isn't really effective enough to be worth fixing if it were to break.
LLM Tarpit
Introduction
With the exception of the occasional social media post I've not written about this before.
The crawlers that are used to train AI models have proven to be a scourge of the internet. So many companies are caught up in gold-rush fever and operate crawlers which collectively overload services intended for humans.
Previously, I deployed Anubis in front of some of my services, however it only really catches the lower hanging fruit and at the cost of inconveniencing actual humans.
Although I still have Anubis deployed in various places, many of my sites now quietly advertise a path into an LLM maze too. The maze is operated by a flask application which runs on a different origin to the rest of my sites.
Hidden links provide a path in but, via standard attributes, also make it clear that no-one automated is authorised to go there:
<a href="/secretstuff/" rel="nofollow noindex" style="display: none">Ignore this</a>
The maze is also disallowed in robots.txt:
User-agent: *
Disallow: /secretstuff/
What this means is that no ethical crawler should ever need to worry about falling into my maze.
But, AI crawlers blatting services into oblivion tells us everything that we need to know about how ethical some of the operators are likely to be (and... cough... it's not just small names).
SSH tarpits generally work by very slowly returning a server banner of infinite length. That's not so easily achieved with HTTP because browsers and intermediate caches enforce timeouts.
But web crawlers present a different value proposition to SSH bots: they end up in the maze because the greedy operators are desperate to consume content to train their models on, regardless of the original author's expressed desires.
So, rather than trying to waste their time by responding slowly, it's far more effective to serve them a smorgasboard of fake content and images so that their crawling doesn't always yield a reward:

Each page in the maze links to at least one other, and generated pages are cached for a short period so that they remain the same if re-fetched for verification.
Embedded images are a mix of real images from elsewhere on my site and fake JPEGs.
The hope, is that crawlers fall into the maze early and so spend their time consuming gobbledygook rather than my actual content.
All of this, of course, can be defended against by the companies operating crawlers - however, doing so increases the cost of training. The far cheaper option, of course, would be to respect robots.txt in the first place.
Activity Rates
Remember how I said that no ethical crawler would ever be in the maze?
from(bucket: "telegraf/telegraf_low_granularity")
|> range(start: -90d)
|> filter(fn: (r) => r._measurement == "fck_your_llm")
|> filter(fn: (r) => r._field == "count")
|> group()
|> sum()
In the last 90 days, the maze has served 310,248,945 requests. That's a lot of unethical activity.
Interestingly, activity has been trailing off since mid-July:

That trail off, though, is partly driven by exceptions being thrown as the result of crawlers which don't set a user-agent header. The untrapped exceptions trigger a server restart, affecting all queued requests and any buffered statistics.
The reduction also follows an unprecedented peak in activity:

Each datapoint is 12 hours apart, so the maximum recorded rate was around half a million requests an hour.
The total number of requests served in that time-frame is 627,732,698 about 49% of that occurred within the last 90 days.
Headless Browsers
Back in January, I noticed that a proportion of crawlers weren't simple bots but were instead using headless browser instances (which allow them to execute javascript in order to get past defences like Anubis):

Javascript executing crawlers represent quite a small proportion of the maze's activity (661668 of the 627732698 requests: 0.1%), but they also represent a subset that is much easier and more fun to fuck with.
As they were clearly hungry for it, I started serving some javascript:

The traps themselves are (mostly) built around the same intent as an SSH tarpit: the aim is to try and tie up the attackers resources so that you increase the computation cost of what they're doing (and hopefully, as a side effect, reduce their efficacy rate).
The maze embeds some javascript which decides (at random) whether to spring a trap and, if so, which one.
Traps include:
-
Amplify: Takes the page content and extends it, injecting lots of extra words into the DOM -
Brash: An implementation of the Brash Browser DoS Attack (no longer in use) -
MemGrow: Repeatedly concatenates strings -
Primes: Factoring of multiple large primes, in parallel
With the exception of Brash, the aim isn't to crash the browser but to significantly increase the resources required to complete a crawl of the page.
Conclusion
Over time, SSH Tarpits have continued to be less effective per-connection. The rates at which they see connections has increased, but bots increasingly enforce client-side timeouts and don't remain stuck for very long.
Until mid-last month, my LLM maze was serving a pretty significant number of requests each day. About 0.1% of requests came with a crawler willing and able to execute javascript: a tiny minority but still a fun one to play with.
I haven't been collecting stats for long enough to say for sure, but it seems reasonable to speculate that the trail-off in maze requests is probably a sign of one (or more) AI companies finishing their training run and preparing to release new models.
It'll be interesting to see whether activity picks back up over the next few months.
Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.