PHP Honeypot Tutorial: Zero-Friction Spam Prevention

A honeypot is a hidden form field that humans never see but bots fill in automatically. When a submission includes data in the honeypot field, you know it came from a bot. Zero user friction, zero external dependencies, fully accessible. For the strategy behind this technique, see our honeypot spam protection guide.

Here are three honeypot techniques with PHP 8.0+ code, from basic hidden fields to JavaScript-enhanced detection.

Basic Honeypot: Hidden Field

<form method="POST" action="/submit.php">
  <label for="email">Email</label>
  <input type="email" name="email" id="email" required>

  <label for="message">Message</label>
  <textarea name="message" id="message" required></textarea>

  <!-- Honeypot field — hidden from humans -->
  <div style="position:absolute;left:-9999px;top:-9999px" aria-hidden="true">
    <label for="company_url">Leave blank</label>
    <input type="text" name="company_url" id="company_url"
           tabindex="-1" autocomplete="new-password">
  </div>

  <button type="submit">Send</button>
</form>
<?php
// Basic honeypot check — PHP 8.0+
$honeypotValue = trim($_POST['company_url'] ?? '');

if ($honeypotValue !== '') {
    // Bot detected — silently accept (don't reveal the trap)
    http_response_code(200);
    // Optionally log: error_log("Honeypot triggered from {$_SERVER['REMOTE_ADDR']}");
    exit;
}

// Proceed with legitimate form processing...

Why These Specific Attributes Matter

Choosing the Field Name

Use a name that looks like a real field to a bot: company_url, phone_number, address2, fax. Avoid names like honeypot or trap — bots can pattern-match those. Rotate the field name periodically if you're facing targeted attacks.

Advanced: Timing Honeypot

Bots submit forms in under a second. Humans take at least 3-5 seconds. Combine the hidden field with an HMAC-signed timestamp to catch fast submissions:

<?php
// Timing honeypot — PHP 8.0+
define('FORM_SECRET', getenv('FORM_SECRET') ?: 'change-this-to-32-random-chars!!');

// Call when rendering the form
function honeypotTimingField(): string
{
    $time = (string) time();
    $sig  = hash_hmac('sha256', $time, FORM_SECRET);
    return $time . '.' . $sig;
}

// Call when processing the submission
function isHoneypotTriggered(array $post, int $minSeconds = 2): bool
{
    // 2 seconds minimum — accounts for browser autofill, which can submit faster than manual typing

    // Check hidden field
    if (!empty(trim($post['company_url'] ?? ''))) {
        return true;
    }

    // Check timing
    $token = $post['_hp_ts'] ?? '';
    $parts = explode('.', $token, 2);

    if (count($parts) !== 2) {
        return true; // Missing or malformed token
    }

    [$time, $sig] = $parts;

    if (!hash_equals(hash_hmac('sha256', $time, FORM_SECRET), $sig)) {
        return true; // Tampered
    }

    $elapsed = time() - (int) $time;
    if ($elapsed < $minSeconds || $elapsed > 7200) {
        return true; // Too fast or too old (2-hour max)
    }

    return false;
}

Form HTML

<form method="POST" action="/submit.php">
  <!-- Visible fields -->
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>

  <!-- Honeypot hidden field -->
  <div style="position:absolute;left:-9999px;top:-9999px" aria-hidden="true">
    <input type="text" name="company_url" tabindex="-1" autocomplete="new-password">
  </div>

  <!-- Timing token -->
  <input type="hidden" name="_hp_ts" value="<?= htmlspecialchars(honeypotTimingField()) ?>">

  <button type="submit">Send</button>
</form>

Handler

<?php
if (isHoneypotTriggered($_POST)) {
    // Silently "accept" — show the same thank-you page a real user would see
    // WARNING: If your honeypot has false positives (autofill, accessibility tools),
    // legitimate submissions will be silently lost. Monitor your trap rate.
    // This prevents bots from learning that they were caught
    header('Location: /thank-you');
    exit;
}

// Process the real submission...

Page caching warning: If your site uses full-page caching (Cloudflare, Varnish, WP Rocket), the PHP-rendered timestamp will be stale. In that case, generate the timing token via JavaScript on page load instead:

// For cached pages: generate timing token client-side
document.addEventListener('DOMContentLoaded', function() {
  const time = Math.floor(Date.now() / 1000).toString();
  // Send time to a small PHP endpoint that returns the HMAC, or use a shared secret
  // approach suitable for your caching setup
  document.querySelector('[name=_hp_ts]').value = time + '.client';
});

Adjust your server-side verification to handle client-generated timestamps appropriately.

Advanced: JavaScript-Enhanced Honeypot

Add a JavaScript-computed value to distinguish browsers from simple HTTP bots. Bots without JavaScript engines can't compute the expected value.

<!-- Add to your form -->
<input type="hidden" name="_js_check" id="jsCheck" value="">

<script>
  // Compute a value that proves JS executed
  document.getElementById('jsCheck').value =
    btoa(document.querySelector('[name="_hp_ts"]').value);
</script>
<?php
// Server-side: verify JS token matches expected value
$timingToken = $_POST['_hp_ts'] ?? '';
$jsCheck     = $_POST['_js_check'] ?? '';
$expectedJs  = base64_encode($timingToken);

$hasJavascript = ($jsCheck !== '' && $jsCheck === $expectedJs);

if (!$hasJavascript) {
    // No JavaScript — likely a simple bot, but could be a privacy-conscious user
    // Don't hard-block; flag for additional checks
    $suspiciousFlags[] = 'no_js';
}

Important: Don't hard-block users without JavaScript. Some legitimate users disable it for privacy. Use the JS check as one signal in a scoring system, not a gate.

Note: the btoa() encoding used here is trivial to reverse-engineer. It stops generic bots that don't execute JavaScript at all, not targeted bots whose authors inspect your source code.

What Honeypots Don't Stop

Be realistic about limitations:

Honeypots are a first-line defense. When bots start getting through, escalate to Cloudflare Turnstile or another CAPTCHA solution.

Honeypot vs CAPTCHA: When to Use Each

Scenario Honeypot Sufficient? Need CAPTCHA?
Low-traffic contact form Yes — handles most spam No
Newsletter signup Usually Add if spam increases
Login page Not enough alone Yes + rate limiting
Registration form Not enough alone Yes
Payment/checkout Not enough alone Yes + rate limiting
High-traffic public form Start here, escalate Likely needed eventually

Verdict

Honeypots are the highest-ROI spam prevention technique for PHP developers. They cost nothing, add zero friction, work with every form framework, and stop the majority of automated spam. Start every new form with a honeypot + timing check. Add a CAPTCHA only when you need to.