Skip to content

My Emails Are Landing in Spam — UI-First Diagnostic

Spam-folder placement happens for measurable reasons. Walk the Insights page, the Sending Server config, and the Test diagnostic — three UI checks isolate the cause before anyone has to look at headers.

What to check from the dashboard

Spam-folder placement isn't a single failure — it's a signal that something measurable is off about your sender setup or your audience engagement. AcelleMail's dashboard exposes the three signals receivers use to make the spam vs. inbox decision.

Open a recently sent campaign and click Insights (or Subscribers → Engagement tab):

Campaign Insights — per-recipient engagement signal

What you're reading:

  • Open rate < 5% for a list you regularly engage → recipients aren't seeing it in their inbox at all. Spam placement likely.
  • Open rate normal but click-through 0 → message rendering broken (suspicious content) or recipients are opening, scanning, and not engaging. Reputation drift.
  • High open rate but high unsubscribe + complaint rate → content mismatched to subscriber expectations. ISPs penalize this.

Open the sending server configuration

Most spam-folder cases trace back to the sending side. Settings → Sending servers → click your active server:

Sending server detail — deliverability config

Three things to verify here:

  1. Authentication status — SPF / DKIM / DMARC verification chips should all be green. Red means the receiving servers can't confirm you're authorized to send for the domain.
  2. Daily quota — if you're hitting the quota every send, sends spill over into the next day's batch. Some ISPs interpret this as bulk-spam pattern.
  3. Bounce + complaint rate (last 24h) — if either is >5%, ISPs throttle your IP. Sustained high rates push you into spam folders.

Run the test diagnostic

Inside the sending server detail, Test button sends a one-off message you can inspect:

Sending server test page — diagnostic send

Send to your own address. Open the email in Gmail / Outlook / Apple Mail.

  • Email arrived in Inbox? → sending setup is OK. The spam problem is content / recipient-side (engagement, subject lines, frequency).
  • Email arrived in Spam? → sending setup is the issue. Move to operator section.
  • Email didn't arrive at all → outright blocked. IP is on a blocklist. Operator section.

In Gmail, click the email → three-dot menu → Show original. The headers reveal:

  • Authentication-Results: ... spf=pass dkim=pass dmarc=pass (all three should pass)
  • Received-SPF and DKIM-Signature presence

If any of the three says fail or is missing, that's your fix target.

Common UI causes and the fix from the dashboard

What you see Likely cause What to do
Sending server: red SPF / DKIM / DMARC chip DNS records missing or incorrect Click Verify → AcelleMail walks you through the DNS records to add. Re-verify after publishing.
Insights: open rate dropped 40% in last 3 sends IP reputation degrading Pause campaigns. Run Subscribers → Email verification on the list. Investigate recent content for unsubscribe spikes.
Test diagnostic lands in spam, headers show "spf=fail" SPF record doesn't include your sending IP / provider Update SPF record at your DNS host. Add the sending vendor's SPF include (e.g. include:amazonses.com).
Test diagnostic lands in spam, body has spam-trigger words Content filtering Re-review subject line + first paragraph. Avoid ALL CAPS, excessive punctuation, link-only content.
Test diagnostic doesn't arrive at all IP blocklisted Check the sending IP at mxtoolbox.com → blocklist lookup. Request delisting per the blocklist's process.

Pause and isolate the bad cohort

If only some recipients land in spam, segment the list:

  • Subscribers → Filter by domain → check open rates per ISP (Gmail / Outlook / Yahoo / corporate).
  • A 0% open rate on @gmail.com while @outlook.com is normal → your Gmail-side reputation is hurt. Investigate recent send patterns to Gmail addresses.
  • Pause sends to the problem segment until you've remediated content / reputation.
Advanced: SPF / DKIM / DMARC tuning for operators

The three authentication standards form the deliverability foundation. All three must pass for ISPs to trust the sender.

SPF (Sender Policy Framework — RFC 7208)

A DNS TXT record listing which IPs / domains are authorized to send for your domain. Lookup:

dig TXT yourdomain.com +short | grep spf
# Expected: "v=spf1 include:amazonses.com -all"

Common gotchas:

  • Multiple v=spf1 records on the same domain — SPF requires exactly ONE. Merge into one record.
  • ~all (soft fail) instead of -all (hard fail) — -all is preferred for production.
  • Sending IP not in the record. Use dig to verify; add the include statement.

DKIM (DomainKeys Identified Mail — RFC 6376)

Cryptographic signature on every outgoing message. The public key is published in DNS at <selector>._domainkey.yourdomain.com. AcelleMail generates the keypair via the sending-server's Verify domain wizard.

# Check the DKIM record exists
dig TXT default._domainkey.yourdomain.com +short
# Expected: "v=DKIM1; k=rsa; p=MIGfMA0..."

Common gotchas:

  • DNS record published but with line breaks — receivers can't parse. Re-publish as single-line.
  • Selector mismatch — AcelleMail uses <servername>._domainkey by default; if you customized the selector during install, verify the record name matches.

DMARC (Domain-based Message Authentication, Reporting & Conformance — RFC 7489)

Tells receivers what to do when SPF or DKIM fails:

dig TXT _dmarc.yourdomain.com +short
# Expected: "v=DMARC1; p=quarantine; rua=mailto:[email protected]"

Migration path (always start permissive, ramp to strict):

  1. p=none; rua=mailto:[email protected] — monitor; receivers send aggregate reports, no policy applied.
  2. After 2-4 weeks of clean reports → p=quarantine; pct=10 — 10% of failing mail goes to spam.
  3. Ramp pct to 100 over 2 weeks → switch to p=reject only after 100% pass rate.

Going straight to p=reject without ramping breaks ALL your mail if any auth is misconfigured.

Inspecting Authentication-Results of a received message:

# Send a test → grep the headers
grep -E "Authentication-Results|Received-SPF|DKIM-Signature" /path/to/saved-email

Look for dmarc=pass AND spf=pass AND dkim=pass. Any failure → fix that side specifically before retrying.

Engagement-driven reputation — ISPs increasingly weight subscriber engagement over pure auth. Even with green SPF/DKIM/DMARC, if your open rate is <10% over a 30-day window, Gmail / Outlook may demote you. Mitigation: aggressive list hygiene (unsubscribe inactive subscribers after 6 months), re-engagement campaigns to dormant cohorts before re-engaging them in regular sends.

Blocklist monitoring — periodic check at mxtoolbox.com for your sending IP + sending domain. Major lists: Spamhaus SBL/XBL/CSS, SpamCop, Barracuda. Each list has its own delisting form.

Related articles

28 comments

18 comments

  1. Sarah
    One more thing worth adding: if you use Cloudflare in front, make sure to whitelist the worker IPs for outbound — we had retries failing because Cloudflare was rate-limiting our own outbound traffic to SES
    1. Admin
      Worth flagging, though I'd want to pin down the mechanism before it goes in the article. Cloudflare proxying your web app shouldn't touch outbound SMTP/API traffic to SES at all, so if retries were failing there it was likely Cloudflare Spectrum, a Zero Trust gateway policy, or a WAF rule on the egress path rather than the regular orange-cloud proxy. If you still have the error from the failed retries, send it over and I'll write up the actual case. Either way the symptom is real and it's the kind of thing that reads as a deliverability problem when it's really a transport one, which is exactly what this diagnostic should catch.
  2. Vera
    We hit cause #5 last quarter — SES sandbox limits we didn't know about. The 'wait it out' advice is right. We tried aggressive retries first and it just made things worse.
  3. Ahmed
    Is there a way to detect cause #6 (lost DB connection) before workers wedge? Looking for a heartbeat metric to alert on.
    1. Admin
      No built-in metric for it. Alert on the age of the newest row the workers write, not on the process being up. A wedged worker stays alive and just stops committing.
  4. Minh
    bookmarking this. wish i had it last month when our queue backed up on a sunday night.
  5. Anna
    Pro-tip: set `pm.max_children` on your PHP-FPM pool to at least 2x your supervisor worker count. Otherwise even ack-fast endpoints choke when the queue rate goes up.
  6. James
    Cause #2 (dead supervisor) hit us after a kernel-upgrade reboot. The systemctl enable bit was missing. Took 2 hours to figure out because nothing was logging
  7. Rafael
    Confirming the campaign:rerun auto-fix actually works. We had a worker OOM mid-batch last week, walked away thinking we'd need to manually intervene, came back in 15 minutes and it had recovered itself.
  8. Ravi
    When you say to bump wait_timeout to 86400, does that need a MySQL restart or is t dynamic? We're on RDS so restarts are expensive. 👀
    1. Admin
      Dynamic, no restart. On RDS change it in the parameter group. Only affects new connections though, so bounce your queue workers.
  9. Chen
    Thanks for grounding this in actual source — much better than the generic Laravel advice you find on Stack Overflow.
    1. Admin
      Most of that advice treats it as an SMTP config problem. It's nearly always SPF/DKIM alignment or list hygiene, and both show in the UI.
  10. Lucas
    sent this to my whole ops team. should be required reading before anyone touches the prod queue
  11. Sofia
    I think the recommendation to wait 10-15 minutes is too patient for production. We alert at 5 minutes and our ops team triages. Stuck status > 5 min usually means something real.
    1. Admin
      The 10-15 minute number is written for someone sending their first campaign, where a queue that hasn't drained yet looks identical to a real fault and people start restarting things they shouldn't. If you have an ops team watching, 5 minutes is the right call. Stuck past that with no delivery log movement is almost always a worker that died or a sending server rejecting silently, not backlog. I'll split that step in the article into two thresholds with the reasoning attached, rather than one number that's wrong for half the readers.
  12. Brian
    This article saved me about 4 hours of debugging oday. The diagnostic order at the top is exactly the workflow I needed
  13. Tomas
    For anyone running on EC2 t3.small — bump to t3.medium before you scale past 50k subs. We learned the hard way that worker OOM kills explode when MySQL is on the same instance.
    1. Admin
      The MySQL-on-the-same-box part is the real killer. t3.small gives you 2GB, and once the sending queue workers each hold a campaign's subscriber page in memory alongside InnoDB's buffer pool, the OOM killer picks whichever process looks fattest, which is usually a worker mid-send. That looks like random delivery failures, not a memory problem, so people chase the wrong thing for a week. Two things worth adding to your setup if you haven't: move MySQL off to RDS (even the smallest instance) before you touch the app instance size, and cap worker concurrency rather than assuming more workers means faster. Past a point you're just multiplying resident memory against the same SMTP throughput ceiling. Worth saying though, none of this is a spam-folder cause, which is what this article is about. OOM'd workers make sends stall or retry, but they don't hurt your inbox placement by themselves. The one indirect link is if retries cause duplicate sends to the same recipients, which does generate complaints. If you saw that, tell me and I'll look at the retry path.
  14. Priya
    curious if the 200-row / 32-advance bounds are configurable. we have one customer with very large automation flows and i wonder if they hit this. fwiw
    1. Admin
      Not configurable today, they're compiled into the runtime rather than read from config. Honest answer on whether a large flow actually trips them: I'd have to check, because I don't know offhand where the limit bites versus where it just paginates. If you can tell me roughly how many steps and branches that customer's biggest flow has, I'll run it against the bounds and come back with a real number instead of a guess. If it does hit, making them env-tunable is a small change and I'd rather do that than have you restructure the flow around an arbitrary constant.
  15. Quân
    If you're on Ubuntu 22.04 with the default cron package, the time zone is UTC even if /etc/timezone says otherwise. Bit us once — the scheduled job timing was 7 hours off
    1. Admin
      That one is nasty because nothing errors, the job just fires at the wrong hour and you only notice when open rates look off. cron reads TZ from its own environment, not /etc/timezone, so unless you set CRON_TZ or restart the daemon after a timedatectl change you stay on UTC. Worth checking with `systemctl show cron -p Environment` rather than trusting the file. For anyone landing here from the spam side: send timing won't put you in spam by itself, but a campaign scheduled for 9am local that goes out at 2am does skew the early engagement signal, and that does feed reputation. I'll add a note about it to the article.
  16. Aditi
    question: in step 4, the campaign log line about 'force resuming' — does that show up in laravel.log or only the per-campaign log file? our laravel.log seems silent on this fwiw
    1. Admin
      Per-campaign log file, not laravel.log. The campaign runner writes to its own channel so a long send doesn't drown out everything else in the main log, which is why yours looks silent. Look under storage/logs for the campaign's own file rather than tailing laravel.log. If you want that line mirrored into laravel.log too, that's a logging channel config change on your side, we don't do it by default. I'll add a note to step 4 saying which log to tail, the article is ambiguous there.
  17. Aisha
    adding to this: we had a campaign stuck for 6 hours one time. Turned out the running_pid was alive but the worker was deadlocked on a slow MySQL query. ps showed it as running, kill -9 was the only fix. Now we monitor for stale running_pid > 30 min.
  18. David
    What about the case where campaign:rerun itself crashes silently? We had cron running but :reru was failing on a deleted customer and just bailing out. No alerts.
    1. Admin
      That's a real gap and it's not really a spam-folder problem, it's a "your sending stopped and nobody told you" problem, which ends up looking the same from the outside. Two things worth checking on your side first. If your crontab line ends in `>> /dev/null 2>&1`, any fatal from the scheduler is going straight to the void. Point it at a file instead and you'll usually see the actual stack trace. Second, look in `storage/logs/laravel.log` around the time it stopped, and at the `failed_jobs` table if the work was queued. An orphaned campaign whose customer row is gone tends to blow up on a null relation rather than exit cleanly, so there is normally a trace somewhere. The part I can't defend is that there's no alert. A command that bails on one bad record should log it loudly and keep going for the rest, not stop the run. I'll take a look at how the orphan case is handled and make it skip plus log rather than die. If you still have the trace from your incident, send it to [email protected] and it'll save me guessing which null it hit.

More in Troubleshooting