Math CAPTCHA in PHP

What is a math CAPTCHA?

A math CAPTCHA asks the user to answer a simple arithmetic question — something like "What is 4 + 7?" The server generates two random numbers, stores the correct answer in the session, displays the question in the form, and compares the submitted answer when the form is posted.

No GD extension, no external API, no JavaScript requirement. The entire implementation fits in around 20 lines of PHP. It is the simplest server-side spam protection you can add to a form after a honeypot field.

How it compares to other methods

Method User friction PHP complexity External dependency Effectiveness
Honeypot None Trivial None Medium
Math CAPTCHA Low Trivial None Medium
Securimage (image CAPTCHA) Medium Medium None (self-hosted) High
Cloudflare Turnstile None Easy Cloudflare Very high

A math CAPTCHA sits in the same effectiveness tier as a honeypot. Where they differ: a honeypot is completely invisible to users (no friction at all), while a math CAPTCHA asks the user to do one small thing. The payoff for that small friction is that math CAPTCHAs catch a slightly different category of bot — ones that fill in all fields but do not attempt to parse and solve on-page challenges.

Implementation

The implementation has three parts: generating the question (on the page that displays the form), rendering it in HTML, and checking the answer in the form processor.

Generate the question when the page loads:

<?php
session_start();

// Generate two random numbers for the math question
$num1 = rand(1, 10);
$num2 = rand(1, 10);
$_SESSION['math_captcha_answer'] = $num1 + $num2;

Render the question in your HTML form:

<label for="captcha_answer">
  What is <?= $num1 ?> + <?= $num2 ?>?
  <span class="required">*</span>
</label>
<input type="number" id="captcha_answer" name="captcha_answer"
       min="0" max="20" required />

Check the answer in your form processor:

<?php
session_start();

$submitted = (int) ($_POST['captcha_answer'] ?? -1);
$expected  = $_SESSION['math_captcha_answer'] ?? null;

// Clear the answer from session immediately — single use only
unset($_SESSION['math_captcha_answer']);

if ($submitted !== $expected || $expected === null) {
    $error = 'Incorrect answer to the security question. Please try again.';
    // Re-display the form with $error and generate a new question
    // (fall through to your form rendering code)
} else {
    // Proceed with form processing
}

The unset call is important. Removing the expected answer from the session immediately after checking it means the same session value cannot be reused to replay a valid submission.

Making it harder to bypass

Basic math CAPTCHAs have a known weakness: bots that parse HTML can extract the two numbers from the DOM and compute the answer automatically. A few techniques raise the bar:

Use words instead of digits. Rendering "three plus seven" as prose rather than "3 + 7" in HTML attributes is significantly harder for a simple DOM parser to handle:

<?php
$words = ['zero','one','two','three','four','five','six','seven','eight','nine','ten'];
$num1  = rand(1, 10);
$num2  = rand(1, 10);
$_SESSION['math_captcha_answer'] = $num1 + $num2;
$question = "What is {$words[$num1]} plus {$words[$num2]}?";
// Output $question as HTML text — much harder to parse than "3 + 7 = ?"

Mix in subtraction and multiplication occasionally. Randomising the operation means a bot cannot assume addition:

<?php
$num1 = rand(2, 10);
$num2 = rand(1, $num1); // ensure subtraction result is non-negative
$ops  = ['+', '-'];
$op   = $ops[array_rand($ops)];

$_SESSION['math_captcha_answer'] = ($op === '+') ? $num1 + $num2 : $num1 - $num2;
$question = "What is {$num1} {$op} {$num2}?";

Important caveat: a determined bot with a proper HTML parser and basic arithmetic can still solve math CAPTCHAs regardless of these tweaks. These measures raise the cost of writing the bypass, they do not eliminate it. For stronger protection, combine a math CAPTCHA with a honeypot, or upgrade to Cloudflare Turnstile.

Generating the question as text (harder to parse)

A full word-based implementation that avoids numeric digits entirely in the question output:

<?php
session_start();

$words = [
    0  => 'zero',  1 => 'one',  2 => 'two',   3 => 'three',
    4  => 'four',  5 => 'five', 6 => 'six',   7 => 'seven',
    8  => 'eight', 9 => 'nine', 10 => 'ten',
];

$num1 = rand(1, 10);
$num2 = rand(1, 10);
$_SESSION['math_captcha_answer'] = $num1 + $num2;

// $question is plain prose — no digits, no symbols
$question = 'What is ' . $words[$num1] . ' plus ' . $words[$num2] . '?';

Output $question directly into your label text. The answer is still a number typed into a standard input, so users are not confused — they read "What is four plus six?" and type "10".

When to use a math CAPTCHA

A math CAPTCHA is a good fit for:

  • Very low-traffic sites receiving basic spam that a honeypot alone is not catching
  • Shared hosting environments where you cannot install PHP extensions or call external APIs
  • Adding a second layer on top of a honeypot at zero additional infrastructure cost
  • Accessibility-sensitive contexts where image CAPTCHAs create barriers — a simple text question is screen-reader-friendly

A math CAPTCHA is not recommended for:

  • High-value forms with real financial or data incentive for abuse — sophisticated bots will solve it
  • High-traffic sites where human spam farms are a concern
  • Any form where you want zero user friction — use a honeypot or Turnstile instead

Verdict: A math CAPTCHA takes about 15 minutes to implement from scratch, requires no libraries, and stops most automated bots targeting generic PHP forms. Start here if you are on a basic hosting environment and need something beyond a honeypot. If you still see spam after adding it, the next step is Cloudflare Turnstile (invisible, free, and far more effective) or Securimage (self-hosted image CAPTCHA, no external dependencies). For WordPress sites, see the CAPTCHA alternatives overview instead.