bot-signal is a TypeScript bot detection library designed for both browser and Node.js environments. After a real test with headless Chromium, Playwright markers, a scripted behavioral scenario and suspicious server requests, my verdict is simple: it’s worth trying if you want explainable signals without depending on an external SaaS.
What is bot-signal?
bot-signal is an open source anti-bot toolkit. It doesn’t promise absolute truth about who’s sitting behind the screen. Instead, it computes a suspicion score from several layers:
- instant browser-side signals, such as
navigator.webdriver, HeadlessChrome, software WebGL, leftover Playwright or Puppeteer objects - behavioral signals, such as overly linear mouse movement, too-regular typing, mechanical scrolling, synthetic events
- server-side signals, such as datacenter IPs, scripted User-Agents, suspicious JA3 fingerprints, inconsistent timezones, missing browser headers
This positioning appeals to me because it feels more like a scoring toolkit than a black box. Every signal has a name, a weight and a confidence level. For a security or DevOps team, that’s often more useful than a magic verdict you can’t explain to customer support.
| Tested point | Observed result |
|---|---|
| Repository version | commit db3e7b5, package 2.0.3 |
| Required runtime | Node.js 22 or higher |
| Installation | npm ci, then TypeScript build |
| Unit tests | 168 tests passed |
| Patchright browser tests | 52 passed, 1 headed test skipped in CI |
| Demo UI | served locally then captured in headless Chromium |
| API key | none, no LLM required |
Installing bot-signal
I started by cloning the repository into /tmp, reading the README, the package.json and the tests. First important detail: the package requires Node.js 22. My container defaulted to Node 20, so I ran the scripts with a Node 22 binary.
Another very concrete detail: /tmp was mounted with noexec, which broke esbuild’s installation inside node_modules. I kept the reference clone in /tmp and ran the actual installation from a temporary copy on an executable volume. That’s a sandbox issue, not a bot-signal issue.
The relevant commands look like this:
git clone https://github.com/okasi/bot-signal.git /tmp/bot-signal
cd /tmp/bot-signal
npm ci
npm run typecheck
npm run test
npm run build
With Node 22 and an executable directory, the base stack passes cleanly: typecheck OK, 168 unit tests OK, ESM, CJS and types build OK.
For the browser side, I installed the Chromium version expected by Patchright, then re-ran the dedicated tests. Without an X display, the headed test fails as expected. In CI mode it gets skipped and the suite passes: 52 tests passed, 1 skipped.
npx patchright install --with-deps chromium
CI=true npm run test:patchright
bot-signal in practice
I didn’t want to stop at npm test. I set up a mini scenario covering three angles.
First angle: the demo web page generated by the project. I served it locally, opened it in headless Chromium via Patchright, then ran detectInstantClient(window) inside the page. Result on my headless Chromium: isLegitClient: false, score 0.986, high confidence. The triggered signals were telling: isHeadless, isSoftwareRenderer, isMissingChromeObject, isSuspiciousWindowDimensions and isEmptyPlugins.
Next, I injected a Playwright marker into the page: window.__playwright__binding__. The score jumped to 1.000, with automation.kind: playwright. That’s exactly what I expected: when an explicit artifact is present, bot-signal doesn’t stay vague about it.
Second angle: behavior. I created a session sample with 16 perfectly aligned mouse movements, 8 identical scrolls, 9 keystrokes at a fixed interval and one synthetic click. The library returned isLegitClient: false, score 0.829, high confidence. Triggered signals: linear-mouse-movement, linear-scroll, linear-typing, synthetic-events.
Third angle: the server side. I tested three contexts:
curl/8.7.1from an AWS range, score0.863, detected ascurl- Chrome User-Agent from AWS, Paris timezone, JA3 flagged as suspicious, score
0.971 - Google DNS with consistent browser headers, score
0.45, still considered legitimate at the default threshold
That last case is interesting. A single timezone inconsistency isn’t enough to trigger a block. That’s healthy, because a VPN or travel can produce that kind of noise.
Does bot-signal actually work?
On obvious cases, yes. Headless Chromium is caught. An explicit Playwright artifact is caught. Cartoonishly robotic behavior is caught. A curl User-Agent is attributed correctly.
What I appreciate most is that the score isn’t binary the moment a weak signal appears. The Google DNS case with a suspicious timezone stays legitimate at the default threshold. That’s exactly the kind of nuance you need in production, otherwise your anti-bot layer becomes a false positive generator.
That said, you have to stay realistic. A modern bot can harden its browser, smooth its movements, route through a residential IP and align timezone, locale and headers. bot-signal doesn’t replace a complete defense stack with rate limiting, adaptive challenges, logging, application-level reputation and business rules.
What I like about bot-signal
I like the readability. Signal names are clear enough to write a rule, an alert or an explanation to support staff.
I also like the fact that the package works without an API key. IP lists and GeoIP data are bundled, so you’re not sending every request to a third-party service. For a moderate-traffic site, that’s simple to operate.
The dual browser and Node usage is well thought out. On the client side you can do an early pass. On the server side you can cross-reference with the IP, headers and TLS fingerprints provided by a trusted reverse proxy.
Finally, the tests are solid for a small project. The Patchright scenarios cover Playwright injections, ChromeDriver, Selenium, PhantomJS, linear behaviors and evasion cases.
bot-signal’s limitations
The first limitation is the environment. The package requires Node 22, which may force upgrades in stacks still running Node 20 LTS on the application side.
The second limitation concerns TLS. bot-signal knows how to use a JA3 or JA4 fingerprint, but it can’t generate one itself. Your edge or reverse proxy needs to compute the fingerprint and inject it into a trusted internal header. If you accept a JA3 header sent directly by the client, it’s worthless.
The third limitation is the risk of over-interpretation. A datacenter IP isn’t necessarily a hostile bot. An inconsistent timezone isn’t necessarily fraud. These signals should be treated as weights, not as legal evidence.
Should you adopt bot-signal?
My verdict: try it, then adopt it if you need lightweight, explainable anti-bot scoring inside a TypeScript application.
I can see it working well on a public form, a login page, a signup API, a queue, an exposed back-office or a blog dealing with aggressive scraping. I wouldn’t put it alone in front of a critical system, but I’d happily add it as a signal layer within a broader strategy.
For a freelance security or DevOps engineer, it’s a solid building block: easy to read, easy to test, transparent enough to audit and concrete enough to produce real signals from the very first run.
FAQ
Does bot-signal automatically block visitors?
No. The library returns a score, signals and an isLegitClient verdict. It’s up to you to decide whether to block, challenge, log or let through.
Does bot-signal detect Playwright and Puppeteer?
Yes, when artifacts are visible, and it also picks up more generic symptoms like HeadlessChrome or software rendering. A well-hardened Playwright setup can reduce those traces, though.
Does bot-signal replace Cloudflare Bot Management?
No. It’s an open source application library, not a full edge protection platform. It’s useful for enriching your decisions, especially if you want to keep the logic inside your own codebase.
