Guide

WordPress contact form submissions not arriving — full diagnosis

A customer messages you on LinkedIn: "I tried to contact you through your website three times last week and never heard back." You check the form yourself. It loads. You fill it in. You hit submit.

1. Problem

A customer messages you on LinkedIn: "I tried to contact you through your website three times last week and never heard back." You check the form yourself. It loads. You fill it in. You hit submit. The page shows the green "Thank you, your message has been sent" confirmation. Your inbox is empty. Spam folder, empty. Promotions tab, empty.

This is the exact moment people start Googling "contact form 7 not sending email" or "wordpress contact form submissions not arriving" or "wpforms email not received" — because the form says it sent, but nothing arrives. There is no error on the page. No red banner in the WordPress dashboard. No notice in the plugin settings. Just silence.

If you run any kind of business through your WordPress site, this failure is invisible until a human tells you. By then, you have lost leads you will never recover.

2. Impact

A broken contact form is not a UX problem. It is a revenue leak with no instrumentation.

The real cost shows up in three places. First, lost inbound — every customer who submits a quote request, support question, or sales inquiry is a person who actively tried to engage with your business and got ghosted by your own infrastructure. Second, brand damage — they do not know your form is broken. They assume you ignored them. Third, compliance gaps — if your form collects user data under GDPR or CCPA and you do not retain a record of submissions, you have no audit trail when a user invokes their right to access.

Sites have run with a broken contact form for six months before noticing. The reason is structural, not negligent — and that is what we are going to fix.

3. Why It’s Hard to Spot

WordPress was designed in 2003 around the assumption that mail() always works because the server has sendmail installed. That assumption stopped being true around 2012 when shared hosts started disabling the binary, when Gmail and Outlook started rejecting unauthenticated SMTP, and when SPF/DKIM/DMARC made unsigned mail invisible to recipients.

WordPress core never caught up. wp_mail() returns true if PHPMailer's send() call did not throw — but PHPMailer considers a successful handoff to a local sendmail process a success, even if that process silently drops the message because the upstream relay rejected it. The function does not wait for delivery confirmation. There is no callback. There is no built-in retry. There is no admin notice if the last 100 emails failed.

The dashboard is worse. The "Site Health" tool checks if wp_mail() is callable, not if mail is actually being delivered. Plugin settings pages for CF7, WPForms, and Gravity Forms show form submission counts, not delivery counts. The two are different numbers and nobody surfaces the gap.

Uptime monitors do not help either. They ping your homepage every minute and report 100% uptime while your contact form silently drops every lead. The form returns HTTP 200. The page renders. Synthetic checks pass. The failure mode is invisible to anything that does not parse the inside of the request.

4. Cause

When someone submits a Contact Form 7, WPForms, Gravity Forms, or Ninja Forms entry, WordPress kicks off a chain of internal calls. The form plugin validates the submission, fires its *_before_send_mail hook, then calls PHP's wp_mail() function. wp_mail() is a thin wrapper around the PHPMailer library. PHPMailer either pipes the message into the local sendmail binary, or — if you have an SMTP plugin installed — opens a TCP connection to an external SMTP relay (SendGrid, Mailgun, SES, Gmail SMTP, etc.).

The Logystera WordPress plugin emits a wp.email signal every time wp_mail() is called, capturing the result. The signal carries a status field with one of four values: delivered, failure, deferred, or missing. A delivered event means PHPMailer's send() method returned true. A failure event means it threw an exception or returned false. A deferred event means the message was handed off to Action Scheduler or wp_schedule_single_event() for later processing. A missing event is the most dangerous — it means a form submission's http.request POST returned 200 but no corresponding wp.email signal was ever fired.

The status field is what reveals where in the chain the failure happened. A wp.email with status=failure and a PHPMailer error message tells you the SMTP handshake or auth step broke. A complete absence of wp.email after a successful form POST tells you wp_mail() was never reached — usually because a plugin filter short-circuited it, or the form plugin queued it via cron and the cron never ran.

5. Solution

5.1 Diagnose (logs first)

Start with the four signals in order. Each one rules out a layer.

Step 1: confirm the form POST actually hit WordPress. The Logystera plugin emits an http.request signal for the form endpoint. Filter for the form's submit URL — for CF7 this is /wp-json/contact-form-7/v1/contact-forms/{id}/feedback, for WPForms it is admin-ajax.php?action=wpforms_submit, for Gravity Forms it is the page POST itself.

grep "POST /wp-json/contact-form-7" /var/log/nginx/access.log | tail -20

You want to see HTTP 200 responses. A 403 means a security plugin blocked the submission. A 5xx means PHP died inside the handler — that produces a php.fatal signal, not wp.email, and you should jump to the WSOD playbook.

Step 2: look for the corresponding wp.email signal. This is the load-bearing signal. In your WordPress installation, enable mail logging by tailing the PHP error log and the Logystera plugin's local history:

tail -f /var/log/php-fpm/www-error.log | grep -iE "phpmailer|smtp|wp_mail"

A successful chain shows a POST /wp-json/contact-form-7/v1/contact-forms/12/feedback 200 immediately followed by a wp.email status=delivered signal with the recipient address. If the POST is there and the wp.email is missing, wp_mail() was never called — a plugin filter killed it, or the form was queued for later via Action Scheduler.

Step 3: if wp.email status=failure, read the PHPMailer error. Check the PHP error log for the php.warning signal that PHPMailer emits on failure:

grep -i "phpmailer\|smtp error\|could not authenticate" /var/log/php-fpm/www-error.log

Real examples of what you will find:

PHP Warning: SMTP Error: Could not authenticate. in /var/www/wp-includes/PHPMailer/SMTP.php on line 661
PHP Warning: SMTP connect() failed. https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting
PHP Warning: stream_socket_client(): Unable to connect to ssl://smtp.sendgrid.net:465 (Connection refused)

Each of these maps to a different fix and we will cover them in section 6.

Step 4: check Action Scheduler if wp.email status=deferred. Some plugins (notably WooCommerce, MailPoet, and certain CRM integrations) queue mail through Action Scheduler instead of sending inline. If those jobs back up, mail goes out hours late or never. Inspect queue depth via WP-CLI:

wp action-scheduler queue
wp eval 'echo as_get_scheduled_actions(["status" => "pending", "per_page" => 5], "ids") |> count();'

A pending count above a few hundred means cron is not draining the queue — that is a wp.cron signal showing missed schedule windows.

Step 5: confirm the form POST → wp.email correlation. This is the highest-signal query of the whole exercise. For every form submission in the last hour, was there a matching wp.email event?

grep -E "feedback|wpforms_submit" /var/log/nginx/access.log | wc -l
grep "wp.email" /var/log/wordpress/logystera-plugin.log | wc -l

If the first number is 47 and the second is 3, you have 44 silently dropped submissions. That gap is the symptom that started this whole investigation.

5.2 Root Causes

(see root causes inline in 5.3 Fix)

5.3 Fix

There are four real causes, in descending order of likelihood. Work through them in order.

Cause 1: SMTP authentication failure. Most common. Symptom: wp.email status=failure with a PHPMailer auth error in php.warning. Cause: rotated SMTP password, expired API key (SendGrid, Mailgun, SES tokens have lifetimes), revoked Gmail app password, or the SMTP plugin lost its config during a plugin update. Fix: regenerate the SMTP credential with the provider, update it in WP Mail SMTP / Fluent SMTP / Easy WP SMTP plugin settings, then send a test email from the plugin's test panel. The corresponding signal: wp.email status=delivered should reappear within seconds of saving the new credential.

Cause 2: deliverability failure (sent but bounced upstream). Symptom: wp.email status=delivered from the WordPress side, but the recipient never gets it. Cause: missing or broken SPF/DKIM/DMARC records, sender domain reputation issue, or the recipient's mail server hard-rejected the message. WordPress thinks it succeeded because PHPMailer handed off cleanly. Fix: check the SMTP provider's bounce log (SendGrid Activity, SES suppression list, Mailgun logs). Add SPF and DKIM records for your sending domain. Move from [email protected] to a dedicated sending subdomain like mail.yourdomain.com if you are getting reputation flagged. Verify DMARC alignment.

Cause 3: plugin filter is short-circuiting wp_mail(). Symptom: form POST returns 200 but the wp.email signal is completely absent — not failed, not delivered, just missing. Cause: a security plugin, anti-spam plugin, or a custom snippet hooked into wp_mail filter and returned false to silently drop the message. Common culprits: WP Hide, custom honeypot plugins, recently deactivated SMTP plugins that left filters registered. Fix: deactivate plugins one by one starting with the most recently updated, retest after each. Or run a quick eval to enumerate filters on wp_mail:

wp eval 'global $wp_filter; print_r(array_keys((array) $wp_filter["wp_mail"]->callbacks ?? []));'

The corresponding signal: wp.email status=delivered reappears once the offending plugin is deactivated.

Cause 4: Action Scheduler queue is stalled. Symptom: wp.email status=deferred events pile up but no delivered events follow them. Cause: WP-Cron is not running because someone disabled it (DISABLE_WP_CRON set to true) and never set up a real system cron, or the cron is running but Action Scheduler jobs are timing out. Fix: confirm WP-Cron status with wp cron event list, set up a real system cron entry hitting wp-cron.php every minute, and bump PHP max_execution_time if Action Scheduler tasks are timing out mid-run. The corresponding signal: wp.cron type=missed_schedule events should stop appearing, and queued wp.email status=deferred should transition to delivered within the next cron tick.

5.4 Verify

The fix is verified when the right signals appear and the wrong signals disappear within a defined window.

Send three test submissions through the live form. Within 60 seconds you should see:

  • Three http.request POST 200 events on the form endpoint
  • Three wp.email status=delivered signals with the recipient address you configured
  • Zero wp.email status=failure signals
  • Zero wp.email status=missing correlations
  • Zero new PHPMailer-related php.warning entries

Then watch under real traffic for at least 30 minutes. Run:

grep "wp.email" /var/log/wordpress/logystera-plugin.log | grep -E "failure|missing" | tail -20

If that returns nothing for 30 minutes under normal submission volume, the issue is resolved. If wp.email status=failure returns within the window, the credential fix did not stick or you have a second cause stacked on top of the first. If wp.email status=missing events appear (form POST 200 but no email signal), a plugin filter is still intercepting and you need to revisit Cause 3.

For deliverability, verification is slower. Send a test to a known external address (a Gmail, an Outlook, a domain you control) and confirm receipt within 5 minutes. Check the inbox, the spam folder, and the SMTP provider's activity log for that specific message ID.

6. How to Catch This Early

Fixing it is straightforward once you know the cause. The hard part is knowing it happened at all.

This issue surfaces as wp.email.

The actual problem was never fixing the form. It was knowing it broke.

A site can run with silently dropping form submissions for weeks because nothing in WordPress core, no uptime check, and no plugin dashboard exposes the gap between "form POST succeeded" and "email actually delivered." The dashboard says everything is fine. The form says everything is fine. The customer who never got a reply is the only signal — and by the time they tell you, the lead is cold.

This is exactly the failure shape that the wp.email signal exists to make visible. Logystera ingests the signal directly from the WordPress plugin every time wp_mail() is called, correlates it with the corresponding http.request form submission, and alerts on three specific patterns: a spike in status=failure, the appearance of status=missing correlations (POST without email), and queue drift in wp.cron for deferred mail. None of these patterns are visible to a synthetic uptime monitor or a server resource graph. They only exist inside the request flow.

You do not need Logystera to debug this once. You can grep your way through it. You need it to know it is happening tomorrow morning before another customer walks away.

7. Related Silent Failures

Email-cluster and form-cluster failures rarely happen in isolation. If you hit one, check the others:

  • WordPress scheduled posts not publishing — same wp.cron type=missed_schedule root cause as deferred email, different surface symptom.
  • WooCommerce order confirmation emails not sent — same wp.email status=failure signal, higher revenue impact, often the first place SMTP rotation breakage surfaces.
  • Password reset emails not arriving — same SMTP path, locks users out, generates support tickets that prove the form has been broken for days.
  • Gravity Forms / WPForms admin notification missing while user notification works — split-recipient deliverability failure, surfaces as partial wp.email status=delivered (one recipient succeeds, the BCC fails).
  • Newsletter / WooCommerce transactional split — Action Scheduler queue depth visible in wp.cron, mail backed up by hours.

See what's actually happening in your WordPress system

Connect your site. Logystera starts monitoring within minutes.

Logystera Logystera
Monitoring for WordPress and Drupal sites. Install a plugin or module to catch silent failures — cron stalls, failed emails, login attacks, PHP errors — before users report them.
Company
Copyright © 2026 Logystera. All rights reserved.