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
position: absolute; left: -9999px— Moves the field off-screen. Don't usedisplay: noneorvisibility: hidden— modern bots check these CSS properties and skip fields that use them.aria-hidden="true"— Tells screen readers to skip the field entirely. Without this, blind users might hear the label and try to interact with it.tabindex="-1"— Prevents keyboard users from tabbing into the field. Essential for accessibility compliance.autocomplete="new-password"— Prevents password managers and browser autofill from filling the field. Some password managers ignoretabindex="-1"and autofill hidden fields, causing false positives for legitimate users.
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:
- Headless browsers (Puppeteer, Playwright): These render your page fully, inspect CSS properties, and skip off-screen fields. Honeypots are invisible to headless browsers the same way they're invisible to humans.
- Targeted attacks: A bot built specifically for your form will eventually learn your field name and timing threshold.
- Human spammers: People paid to submit spam by hand bypass all automated defenses.
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.