Math CAPTCHA in PHP

What is a math CAPTCHA?

A math CAPTCHA asks the user to solve a simple arithmetic problem — "What is 4 + 7?" The server generates two random numbers, stores the answer in the session, shows the question, and checks the response on submit.

No GD extension, no external API, no JavaScript. About 20 lines of PHP. The simplest server-side spam protection you can add 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

Math CAPTCHAs and honeypots stop roughly the same amount of spam. Honeypots are invisible; math CAPTCHAs make the user do something. That extra step catches bots that fill every field but don't bother parsing on-page challenges.

Implementation

Three parts: generate the question, render it in HTML, check the answer.

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
}

Unset the expected answer immediately after checking. Otherwise a bot can replay the same session and skip the challenge entirely.

Making it harder to bypass

The obvious weakness: bots can scrape the HTML, pull out the numbers, and do the arithmetic. A couple of tricks make that harder.

Use words instead of digits. "Three plus seven" is harder to parse than "3 + 7":

<?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 can't just 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: any bot with an HTML parser and basic arithmetic will still solve these. You're raising the effort, not eliminating the risk. For stronger protection, layer a math CAPTCHA with a honeypot, or step up to Cloudflare Turnstile.

Generating the question as text (harder to parse)

A full word-based version — no numeric digits anywhere in the question:

<?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] . '?';

Drop $question straight into your label text. Users read "What is four plus six?" and type "10".

When to use a math CAPTCHA

Use a math CAPTCHA for:

  • Low-traffic sites where a honeypot alone isn't catching enough spam
  • Shared hosting where you can't install PHP extensions or hit external APIs
  • A second layer on top of a honeypot — no extra infrastructure needed
  • Forms that need screen-reader support — a text question works where image CAPTCHAs don't

Do not use a math CAPTCHA for:

  • High-value forms where there's money or data worth attacking — targeted bots will solve it
  • High-traffic sites dealing with human spam farms
  • Any form where you want zero user friction — use a honeypot or Turnstile instead

Verdict: Fifteen minutes, no libraries, stops most dumb bots. Start here if you're on shared hosting and a honeypot isn't enough. Still getting spam? Move to Cloudflare Turnstile (invisible, far stronger) or Securimage (self-hosted image CAPTCHA, no external calls). WordPress users: see the CAPTCHA alternatives overview.