Akismet sits between Drupal's form system and the Akismet cloud API. When a visitor submits a comment, contact form, webform, or registration, Akismet intercepts the submission, sends it to the API for classification, and acts on the verdict — all invisible to the user.
+-----------+ +------------------+ +--------------+ +-------------+
| Visitor |--->| Drupal Form API |--->| Akismet |--->| Akismet |
| submits | | (validate phase)| | Module | | Cloud API |
| form | | | | | | |
+-----------+ +------------------+ +--------------+ +-------------+
| | |
| |<-------------------+
| | verdict: ham / spam / discard / pending
v v
+------------------+ +--------------+
| Entity saved | | Check data |
| (published or | | persisted |
| unpublished) | | to DB table |
+------------------+ +--------------+
Hooks into Drupal's form validate phase
AkismetFormHooks attaches validation handlers to comment, contact, and registration forms. AkismetWebformHandler does the same for webforms. All logic lives in AkismetFormHandler. Runs at module weight 1 — if Honeypot/CAPTCHA already rejected the form, Akismet skips the API call.
Translates Drupal data → Akismet API format
AkismetPayloadBuilder constructs an SDK Content DTO from form values: author, email, body, IP, user agent, language, 14 bot-detection signals, webhook callback URL, and a server-generated nonce.
Client-side behavioral signals
akismet-frontend.js (ported from the WP plugin) passively collects keystroke timing, mouse movement, touch events, scroll counts, and a honeypot field. Stored in sessionStorage, injected as hidden fields on submit. No cookies, no external requests.
Bridge to the automattic/akismet-sdk package
AkismetService wraps the SDK with Drupal concerns: API key resolution (settings.php > Key module > config), event dispatch (PreCheck/PostCheck/Feedback), rate limiting, circuit breaker gating, and subscription queries.
Transfers verdict from validation → entity save
AkismetVerdictStore holds the verdict in memory. AkismetEntityHooks reads it during hook_comment_presave() (unpublishes spam) and hook_comment_insert() (writes check data to the DB).
Custom DB table tracking every spam check
akismet_check_data stores: entity ID/type, result, GUID, original submission (JSON), moderator overrides, retry state, and history. Composite PK (entity_id, entity_type). Powers the spam queue, feedback, recheck, GDPR, and stats.
Admin tools for reviewing Akismet's decisions
Spam tab at /admin/content/comment/spam with Not Spam and Empty Spam actions. Akismet status column + per-row Mark as Spam/Not Spam links on comment admin. Check history on comment view pages. Bulk actions in admin dropdown.
Moderator corrections improve future accuracy
6 action plugins (comment/contact/webform × spam/ham) reconstruct the original payload, set reporter + commentCheckResponse, and call submitSpam() or submitHam(). Failures queue for retry with exponential backoff.
Async resolution when the API needs more time
When check() returns shouldRecheck(), the comment is held as pending. Two paths race: the API POSTs the verdict to /akismet/v1/webhook (fast), or AkismetDeferredRecheckWorker re-checks after the deadline (fallback). Ham publishes; spam stays held.
Protects the site when the API is down
AkismetCircuitBreaker: 3-state machine (closed → open → half-open) backed by State API. Stops making calls when failures spike, probes after a recovery timer. The module always fails open — API problems never block legitimate submissions.
Background housekeeping via cron and Drush
AkismetMaintenanceService runs 5 jobs: recheck error records, expire old payloads (GDPR), purge spam comment entities, clean up orphaned check data, and re-queue stale pending records. Both hook_cron() and Drush delegate to this single service.
| Verdict | What Happens | Comment State |
|---|---|---|
| Ham | Submission proceeds normally | Published |
| Spam | Saved but hidden from public | Unpublished, appears in spam queue |
| Discard | Blatant spam — blocked in strict mode for anonymous users | Unpublished (moderate) or blocked entirely (strict) |
| Pending | Deferred — held until webhook or queue worker resolves | Unpublished until resolved |
AkismetFormHooks --> AkismetFormHandler --> AkismetPayloadBuilder --> AkismetService --> SDK --> API | AkismetVerdictStore <-------------+ | AkismetEntityHooks <----------+ | AkismetCheckData (DB write) AkismetWebhookController --> AkismetCheckData (lookup by GUID, update result) | Entity publish/unpublish side effects AkismetMaintenanceService --> AkismetCheckData + AkismetService (recheck, purge, cleanup) ^ hook_cron() / Drush
The module never touches entities during REST/programmatic saves — only form submissions trigger checks. Everything is DI-wired through services.yml with interface-based contracts for testability and decoration.