Protecting your site from bots without focusing on CAPTCHA: server validation, rate limiting and step-up checks

21.04.20267 min read
Makar Kucherenko
Python developerMakar Kucherenko

If on a site all the protection against bots is still reduced to one CAPTCHA in front of the form, the problem is no longer in the bots, but in the architecture. In 2026, a bot can open a normal browser, wait for the render, go through some of the client logic, change the IP and, if necessary, send a challenge to the solver service. Therefore, the engineering question does not sound like this: “which CAPTCHA to put?”, but like this: “what signals do we check on the server and what do we do if the request looks suspicious.”

The same Cloudflare Turnstile server verification required: The client widget by itself does not protect anything. The documentation directly states that tokens can be faked, they only live 300 seconds and are accepted once. If the server does not call Siteverify, an attacker can send any string to your endpoint instead of a token. This is a typical integration error.

Why one CAPTCHA is not enough

CAPTCHA is usually placed at one point: login, registration, feedback form or checkout. But an attack rarely lives at one point.

Here's what the bot can do in practice:

  • automate the browser via Playwright, Puppeteer or Selenium;
  • distribute requests across residential proxy and ASN, so as not to rely on one IP;
  • send a challenge to an external solver;
  • reuse the normal user flow if the backend only checks the presence of a token, and not its validity;
  • spread your attempts over time so as not to run into a simple limit N запросов в минуту.

Therefore, OWASP identifies not just one “bot type”, but a whole set of automated threats. B OWASP Automated Threats registry listed separately OAT-009 CAPTCHA Defeat and OAT-008 Credential Stuffing, which is defined as mass login attempts to verify stolen login/password pairs.

A simple engineering conclusion follows from this: CAPTCHA can be left as one of the signals, but it cannot be made the center of protection.

What should happen on the server after the challenge

The technically correct scheme looks like this:

  1. The client receives a challenge token.
  2. The client sends a token along with the action: login, registration, lead form, checkout.
  3. Backend validates the token on the server side.
  4. The backend verifies that the token is not expired, not overused, and relates to the expected action.
  5. The backend combines the challenge result with other signals: IP, headers, retry rate, fingerprint, ASN reputation, session quality.
  6. Only after this the server decides what to do: skip the request, ask for a step-up, freeze the action, or send the request for manual verification.

Cloudflare Turnstile in Server Validation Documentation Separately writes three things that are important in the code:

  • the token can be faked if you do not validate it on the backend;
  • token lives 5 minutes;
  • the token is one-time use and reuse should fall from timeout-or-duplicate.

This is important not as an abstract recommendation, but as a concrete rule of integration. If your endpoint accepts a request just because the field cf-turnstile-response generally arrived, the bot has already bypassed the protection.

Minimal implementation: how to validate a challenge correctly

For a form, login or registration, the minimum server flow should be like this:

const response = await fetch(
  'https://challenges.cloudflare.com/turnstile/v0/siteverify',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      secret: process.env.TURNSTILE_SECRET,
      response: token,
      remoteip: clientIp
    })
  }
);

const result = await response.json();

if (!result.success) {
  return rejectRequest('challenge_failed', result['error-codes']);
}

Next, it’s useful to add three more checks:

  • does the expected action match what you expect at the endpoint;
  • does the hostname match;
  • whether the same token arrived twice.

B Google reCAPTCHA recommendations for automated threats the same logic is formulated through action and expectedAction: The action on the page and the expected action during server verification must match. If they do not match, the request cannot be considered normal.

This is where many teams go wrong. The challenge is placed on the front, and the backend does not connect the token with the real action. As a result, you can try to slip the token from registration into another script.

Why checkbox CAPTCHA makes the product worse and hardly helps the architecture

If you need a real reason, not a marketing one, it's very simple: the checkbox challenge itself adds friction, but doesn't provide enough context.

Google's best practices for automated threats he directly writes that checkbox keys increase friction and can affect the conversion rate. In the same optimal scheme, Google recommends not just a challenge, but score-based keys, action binding and separate scripts for login, checkout, account creation and other actions.

That is, even the CAPTCHA provider already suggests thinking not in terms of a widget, but in a risk model:

  • login requires one action;
  • to register another;
  • for checkout third;
  • for account change the fourth.

The same challenge for all scenarios usually means that the protection is just for show.

What to add besides CAPTCHA: basic anti-bot stack for a web application

Below is a stack that is really useful for development.

1. Action-specific rate limiting

Limits should not live at the “entire site” level, but at the level of a specific action.

For example:

  • /login — limit on IP, login identifier and device;
  • /register — limit on IP, email domain and fingerprint;
  • /contact — limit on IP, user-agent and frequency of similar payloads;
  • /checkout — limit by account, cart, payment method and IP.

The most common mistake here is the IP limit only. This is not enough for credential stuffing.

2. Step-up instead of blocking everyone

Not every risk has to end in a block.

Normal flow is like this:

  • low risk - we pass without unnecessary friction;
  • medium risk - enable challenge or email verification;
  • high risk - we ask for MFA, freeze the action or cut the session.

OWASP MFA Cheat Sheet directly states that MFA is the best protection against most password attacks, including credential stuffing and password spraying.

3. Limitation of privileges after registration

Even if a bot has registered an account, this does not mean that it should immediately gain full access.

Good practice:

  • delay in publishing content;
  • limits on the first N messages or requests;
  • restriction on bulk API calls;
  • separate moderation of the first actions;
  • email or phone verification before obtaining sensitive rights.

This is especially useful for forms, personal accounts, marketplaces and B2B services.

4. Honeypot and server-side payload validation

For lead forms and simple contact scenarios, it is often not a “stronger challenge” that helps, but a smarter backend:

  • hidden field honeypot;
  • check for submitting a form too quickly;
  • minimum filling time;
  • failure if payload is repeated too often;
  • normalization of phone, email and URL before recording in CRM.

It's cheap to implement and cuts down junk automated traffic well.

5. Observability, not just blocking

If you don't write events, you're not protecting the system, you're just hoping.

The minimum that is worth logging:

{
  "route": "/login",
  "action": "account_login",
  "ip": "client-ip",
  "asn": "asn-id",
  "fingerprint": "device-hash",
  "challenge_success": true,
  "challenge_errors": [],
  "rate_limit_bucket": "login:ip+email",
  "decision": "step_up",
  "latency_ms": 184
}

This is necessary to then answer normal engineering questions:

  • challenge generally helps or only hinders;
  • tokens fall more often due to attacks or due to the integration curve;
  • which routes attack more strongly;
  • where is more false positive;
  • what scenarios break conversion.

Cloudflare in Turnstile analytics advises looking at at least three basic metrics: Siteverify requests, Valid tokens and Invalid tokens. A large proportion of invalid tokens can mean both bots and broken integration.

Typical integration errors that break security

Here is a list that is most often found in web projects:

Challenge is tested only on the client

This is the biggest mistake. If backend doesn't call Siteverify, the defense is fictitious.

The same challenge for all actions

Login, registration and checkout are different risk scenarios. They cannot be lumped into one action and one policy.

No verification expectedAction

If the action from the front is not verified on the server, the challenge can be used in a place other than where it was issued.

There is no separate strategy for credential stuffing

OWASP describes credential stuffing as a mass verification of stolen login/password pairs. For such a scenario, a CAPTCHA without MFA and rate limit usually only slows down the attack, but does not stop it.

No replay protection

The challenge token cannot be accepted more than once. This is not an "additional option", but part of the threat model.

No fallback logic when an external provider fails

If Cloudflare or another challenge provider has a temporary problem, the backend should have clear behavior:

  • fail closed for high-risk operations;
  • step-up or retry for medium risk;
  • degradation without completely breaking the UX for low-risk scenarios.

Practical anti-bot plan for 2 weeks

If you currently only have CAPTCHA, and there is no normal backend control, a reasonable plan looks like this:

Week 1

  1. Divide routes by risk: login, register, contact, checkout, password-reset.
  2. Enable server-side challenge validation for each sensitive route.
  3. Add action and server-side verification of the expected action.
  4. Enter rate limiting not only by IP, but also by email/login/fingerprint.
  5. Create logs by challenge_success, error-codes, decision, latency.

Week 2

  1. Enable step-up for medium risk.
  2. For logins, add MFA or at least prepare a rollout for high-risk logins.
  3. Add honeypot and delayed privileges for registration and forms.
  4. Check metrics: conversion, invalid tokens, false positive, spam rate.
  5. Run a manual abuse test: replay token, action substitution, quick form submission, massive login attempts.

If you need not a separate widget, but server logic, route control and analysis of anomalies based on events, this is already a task for web development and applied anti-abuse architecture.

Sources to check

  1. Cloudflare Turnstile: Validate the token
  2. Cloudflare Turnstile: Token validation analytics
  3. Google reCAPTCHA: Best practices for protection from automated threats
  4. OWASP Automated Threats to Web Applications
  5. OWASP OAT-008 Credential Stuffing
  6. OWASP Multifactor Authentication Cheat Sheet

Leave your contacts - we will call you back, sort out the problem and offer the best way. We have more than 350 projects behind us, each of which we launched with an individual approach. We guarantee expert advice during business hours.