Panellicense

Exim outbound throttling on cPanel: stop spam before blacklisting

Layered Exim limits, per-account caps, and queue triage on cPanel so one compromised WordPress install can't bury your server's IP on Spamhaus by lunchtime.

8 min readUpdated 2026-05-16exim · cpanel · anti-spam · deliverability
schema: HowToschema: FAQPage

Shared cPanel servers rarely get blacklisted because the operator misconfigured the relay. They get blacklisted because one customer's WordPress plugin shelled out, dumped 80,000 sends into Exim's queue, and SORBS picked it up before anyone noticed. Default cPanel settings will not catch that — outbound caps are off, the nobody user can hit remote port 25, and there is no SpamAssassin pass on outgoing mail.

This is the layered defence you actually need on a production cPanel box: per-account hourly caps, an Exim ACL that blocks PHP from talking to remote SMTP, an outbound SpamAssassin scan, and the exact triage commands for when the queue is already on fire. For inbound reputation (SPF, DKIM, DMARC, PTR), see the cPanel deliverability checklist — this article assumes that side is already done.

Why the defaults don't catch a spam outbreak

cPanel ships with three permissive defaults that combine badly:

  1. Max hourly emails per account is 0 (unlimited) on most fresh installs.
  2. The nobody user — which mod_php and any DSO handler run as — can connect outbound to port 25, so a compromised script bypasses SMTP auth entirely.
  3. SpamAssassin scans inbound only. A script can drop 100 MB of Viagra mail into the queue and Exim will dutifully deliver it.

A compromised PHP file calling mail() or piping to /usr/sbin/sendmail runs as the account's PHP-FPM user (or nobody), hits Exim's local submission path, and inherits the account's effective limits. If those limits are 0, your server's IP is on the SBL inside an hour.

Layer 1: per-account hourly caps in Tweak Settings

In WHM, open Server Configuration → Tweak Settings → Mail. Set the following:

SettingRecommendedWhy
Max hourly emails per domain200Catches a single zone going wild
The percentage of email messages a domain may send per hour that may fail (soft and hard bounces) before the domain is blocked from sending25Real outbound has < 5 % bounce; spam runs 40–80 %
Track email origin via X-Source headersOnLogs the calling script — invaluable for forensics
Initial default/catch-all forwarder destinationfailStop backscatter from typo'd addresses
Restrict outgoing SMTP to root, exim, mailman (FKA SMTP Tweak)OnThe most important toggle on the page

The last one is the single highest-leverage change on this list. It enables an iptables rule that blocks outbound TCP 25 from every user except root, exim, and mailman. A PHP script can still talk to your local Exim — it just can't bypass it by opening a socket to gmail-smtp-in.l.google.com:25 directly.

Layer 2: per-account caps on the account itself

Tweak Settings sets the default. Override per-account from Home → Account Functions → Modify an Account → Max Hourly Email by Domain Relayed, or in bulk via the CLI:

whmapi1 modifyacct user=customer1 MAX_EMAIL_PER_HOUR=300
whmapi1 modifyacct user=customer1 MAX_DEFER_FAIL_PERCENTAGE=20

For accounts on Reseller plans, set sane ceilings in the package (Packages → Edit a Package) so new accounts inherit them. Resellers can raise the limit for their own clients up to the package maximum — you set the ceiling, they tune below it. This is the right place to enforce policy across hundreds of accounts without per-account clicks.

Layer 3: scan outbound mail with SpamAssassin

Inbound-only SpamAssassin is the cPanel default. Flip it for outbound too in Service Configuration → Exim Configuration Manager → Advanced Editor, locate the acl_smtp_data ACL and add a spam clause before the accept verb:

deny
  message     = This message scored $spam_score points. Sending blocked.
  spam        = nobody/defer_ok
  condition   = ${if >{$spam_score_int}{50}{1}{0}}
  log_message = Outbound spam rejected for $authenticated_id score=$spam_score

Score 5.0 (the 50 is tenths) is aggressive for outbound — most legitimate marketing mail will sit between 2 and 4. Tune up if you get false positives from transactional senders, down if a spam run still slips through.

Hosting-grade tuning is where this gets thankless. If you're spending more than a few hours a quarter chasing outbreaks, the paid option is Imunify Email, which adds AI-trained outbound classification and the ability to quarantine rather than reject. The Imunify360 install guide covers the package; Imunify Email is the same vendor and integrates with the same dashboard.

Layer 4: see the queue and find the culprit

You only get to triage if you can see the outbreak. The two commands every cPanel operator should have in muscle memory:

# Total messages currently queued
exim -bpc

# Top 10 senders by queued message count
exim -bp | awk '/^ *[0-9]+[mhd]/{print $4}' | sort | uniq -c | sort -rn | head

Wire the first one into a Nagios/Prometheus check. A healthy shared cPanel server idles under 200 queued messages; alert at 1,000, page at 5,000. By the time you're at 20,000 you're already on at least one RBL.

The second one tells you which account to suspend. The X-Source headers (enabled in Layer 1) tell you which script:

exim -Mvh <message-id> | grep -i x-source
# X-Source: /home/customer1/public_html/wp-content/plugins/old-slider/upload.php
# X-Source-Args: /usr/bin/php /home/customer1/public_html/wp-content/...
# X-Source-Dir: /home/customer1/public_html/wp-content/plugins/old-slider

That's your root cause: an old vulnerable plugin in a known location. Restore the file from backup, remove the plugin, push the customer to the cPanel Site Quality Monitoring tools to keep WordPress patched, and move on.

When the outbreak is already happening

Once the queue is in the tens of thousands, the order of operations is fixed:

# 1. Suspend the source account immediately
/scripts/suspendacct customer1 "outbound spam - ticket 12345"

# 2. Freeze every queued message from that account
exim -bp | awk '$6 ~ /<customer1@/{print $3}' | xargs -n1 exim -Mf

# 3. Delete the frozen spam (verify first with -Mvh on a sample)
exim -bp | awk '$6 ~ /<customer1@/{print $3}' | xargs -n1 exim -Mrm

# 4. Restart Exim to clear in-flight deliveries
/scripts/restartsrv_exim

Do not skip step 1. Until the account is suspended, every minute you spend clearing the queue is a minute the compromised script is generating more mail. Suspend first, triage second.

After the queue is clean, check the major RBLs (Spamhaus SBL/CSS, SORBS, Barracuda) and file delisting requests for any that picked you up. Most of them auto-delist within 24 hours once the source stops, but Spamhaus SBL requires a manual request and a clear explanation of what changed.

Bonus: stop the nobody user sending mail at all

If you're on suPHP, PHP-FPM, or LSAPI handlers, every PHP request runs as the account user, not nobody. The nobody user has no legitimate reason to send mail and you can block it entirely:

# Add nobody to the Exim sender blocklist
echo "nobody" >> /etc/blockcpsmtpgid
/scripts/restartsrv_exim

This will not affect any modern cPanel server — nobody mail is a 2012 problem. But catching the one rogue CGI script that still runs as nobody is a 30-second hardening win. For the broader picture of which handler runs as which user, the PHP handlers on cPanel comparison lays it out.

How do I stop one cPanel account from sending spam?+
Suspend the account with /scripts/suspendacct, then freeze and delete its queued mail with exim -bp piped through awk and xargs to exim -Mf and exim -Mrm. Suspend first — every second of delay generates more queue.
Why does my cPanel server keep getting blacklisted even with outbound limits?+
The Tweak Settings cap is per-account but defaults to 0 (unlimited) on existing accounts created before you changed it. Apply the new cap retroactively via whmapi1 modifyacct on each account, or use Modify Multiple Accounts in WHM.
Does the nobody user need outbound SMTP on any modern cPanel server?+
No. With suPHP, PHP-FPM, LSAPI, or CGI handlers, all PHP runs as the account user. You can safely add nobody to /etc/blockcpsmtpgid and restart Exim.
How do I check what's in the Exim queue right now?+
Run exim -bpc for the total count and exim -bp | awk '/^ *[0-9]+[mhd]/{print $4}' | sort | uniq -c | sort -rn | head to see the top senders. Healthy idle is under 200; alert above 1,000.
Will enabling outbound SpamAssassin slow down legitimate mail?+
Each scan adds 50-200 ms of CPU per message. On a shared server doing under 10 messages/second average, it is invisible. The bigger risk is false-positive rejection of transactional mail — start at a score of 5.0 and tune up if needed.
Is the WHM 'Restrict outgoing SMTP' option safe to enable on an existing server?+
Mostly. It will break any application that opens a direct connection to a remote MTA on port 25 — most often legacy WordPress plugins talking to SendGrid or Mailgun via the wrong port. Submission port 587 with auth is unaffected. Audit by tailing /var/log/messages for blocked connections in the first 24 hours.

Next steps

Switch in an afternoon

Switch from your current reseller — free.

We migrate active cPanel, Plesk, LiteSpeed and CloudLinux licenses from any reseller. We prorate the first month so you never pay twice, and your customers see zero downtime during the swap.