Greylisting compared: standalone Postgrey vs the rspamd module (and how to stop delaying OTP mail)
Postgrey and rspamd's greylist module both stop spam cheaply — but they fail in opposite ways. One keys on exact client IP and gets defeated by Google's sender pools; the other keys on a body hash and only greylists the ambiguous middle. Here's which one to run, and how to tune it so nobody files a "my 2FA code arrived 11 minutes late" ticket.
EvilMail TeamJuly 20, 202610 min read
A customer opened a ticket: the 2FA code from their bank never showed up. Then, eleven minutes later, it arrived — already expired. No bounce, no error, nothing in their spam folder. The bank swore it sent instantly.
The maillog told the real story. The first delivery attempt at 14:02:11 got a 450 4.2.0 from our own Postfix. The bank's MTA queued it and retried from a *different* IP in its outbound pool at 14:13:40, and that one landed. Somewhere between "cheap spam filter" and "we just delayed a time-critical login by eleven minutes" is the entire problem with greylisting.
Greylisting is one of the highest spam-blocked-per-CPU-cycle filters ever shipped. It costs almost nothing, needs no training data, and still kills a huge fraction of bot spam in 2026. But it taxes exactly the mail a temp-email and mail-infrastructure platform can least afford to delay: OTPs, password resets, sign-in links. The goal isn't to rip greylisting out — it's to keep the kill rate while paying almost none of the latency. The two mainstream implementations, standalone Postgrey and rspamd's built-in module, get you there by completely different routes, and one of them is a dead end on modern sender infrastructure.
What greylisting actually does on the wire
The mechanic is deliberately dumb. The first time an unknown sender triplet — (client IP, envelope-from, envelope-to) — tries to deliver, you reject it with a temporary 4xx code. An RFC 5321-compliant MTA treats a 4xx as "try again later" and re-queues the message. A throwaway spam bot usually has no queue, so it just moves on. When the legitimate sender retries after the greylist window, you accept.
Or, from rspamd's milter path, a 451 4.7.1 Greylisting in effect, please come back later.
Here is the detail that generates every "slow mail" complaint you will ever get, and almost nobody configures around it: the delay your customer experiences is not the delay you configured. You might set a 60-second greylist window. Irrelevant. Once you defer the first attempt, the clock is owned by the *sending* MTA's retry schedule. Postfix's minimal_backoff_time defaults to 300 seconds; many commercial senders back off 5, 10, or 15 minutes before the second attempt. Your 60-second window just means the retry *will* be accepted whenever it happens to arrive — it does nothing to make it arrive sooner.
Postgrey: the standalone policy daemon
Postgrey is a small Perl daemon that speaks Postfix's policy-service protocol over a socket — inet:127.0.0.1:10023 by default, or a UNIX socket under private/postgrey. Postfix hands it the connection metadata; Postgrey answers with an action. On a first-seen triplet it returns DEFER_IF_PERMIT, which becomes that 450 on the wire.
You wire it into smtpd_recipient_restrictions, and placement is not optional:
Greylisting goes *last*, after reject_unauth_destination and your RBLs. If you greylist before those checks, you'll cheerfully defer mail you were going to reject outright — burning a five-minute round trip on a message that was never going to be accepted, and teaching spam bots to retry.
The runtime knobs live in /etc/default/postgrey on Debian/Ubuntu:
--delay is the minimum window before a retry is accepted, --max-age expires unused triplets after 35 days, and --auto-whitelist-clients=5 promotes a client IP to a local whitelist after five successful deliveries so it stops being greylisted at all. Whitelists live in /etc/postgrey/whitelist_clients and /etc/postgrey/whitelist_recipients; edit them and systemctl restart postgrey.
Now the fatal flaw. The triplet keys on the full /32 client IP. Google, Microsoft 365, and Amazon SES don't send from one IP — they send from pools of dozens to hundreds. The first attempt comes from 209.85.220.41, gets greylisted, and the retry comes from 209.85.220.73 — an IP that triplet has *never seen*. To Postgrey that's a brand-new triplet, so it greylists *again*. On a large enough pool the delay doesn't resolve in one retry; it compounds. Postgrey ships a bundled whitelist_clients full of provider patterns precisely to paper over this, but that's a chase-the-list game you lose every time a provider adds a netblock.
The rspamd greylist module
Rspamd approaches the same problem from inside the scanner, and makes two architectural choices Postgrey structurally cannot.
It runs off the milter interface, not a policy service. In main.cf:
milter_default_action = accept fails open: if rspamd is down, mail flows rather than bouncing. State lives in Redis (mandatory — the module simply won't greylist without it):
# /etc/rspamd/local.d/greylist.conf
expire = 1d;
timeout = 5min;
key_prefix = "rg";
max_data = 10Kb;
message = "Greylisting in effect, please come back later";
The greylist module only acts on messages that have already reached the greylist action, and that threshold lives with the other action scores, not in the module:
text
# /etc/rspamd/local.d/actions.conf
greylist = 4;
The first difference: rspamd only greylists messages whose computed action is already `greylist`. Mail with valid DKIM and aligned DMARC, or that trips a WHITELIST_SPF symbol, scores low, never reaches the greylist action, and is delivered on the first attempt with zero delay. Postgrey has no concept of a message's reputation — it greylists a perfectly-signed newsletter and a botnet blast with equal enthusiasm.
The second difference: rspamd keys on a hash of the message body and selected headers (bounded by max_data), not the connecting IP. When Google retries the identical message from a sibling pool IP, the body hash is identical, the Redis key matches, and it clears in one retry. Provider IP rotation is completely defeated because the client IP was never part of the key.
Why the delay differs: pools and hashing
Put one inbound message from a Google pool through both systems and the divergence is stark.
That selectivity axis is the whole game. Postgrey delays a DKIM-signed, DMARC-aligned transactional message and a spam blast identically. Rspamd delays neither the signed mail (it never scores into greylist) nor the obviously-rejectable spam (it hits a reject action first) — only the messy middle where greylisting actually earns its keep.
Tuning either one so nobody files a ticket
If you run Postgrey: drop --delay to 60 seconds and don't bother tuning the retry side, because you can't control it. Preload the major providers into /etc/postgrey/whitelist_clients so their pools never get greylisted in the first place — resolve and pin the SPF sources worth trusting: _spf.google.com, spf.protection.outlook.com, and amazonses.com. Keep role recipients like postmaster@ and abuse@ in whitelist_recipients (RFC 2142 says those must not be blocked). Consider --privacy so you're not writing sender addresses to disk in cleartext. And accept that this is maintenance you own forever.
If you run rspamd: don't fight the score gate, lean on it. Make sure the dkim, dmarc, and whitelist modules are pulling their weight so aligned mail skips greylisting entirely, and set the greylist action threshold to 4 in actions.conf so you only trap the band that's genuinely uncertain. Use a Redis TTL that isn't punitive — expire = 1d means a sender that went quiet for a day isn't treated as brand-new forever. And turn on Redis persistence:
text
# redis.conf
appendonly yes
Without it, a Redis restart wipes every greylist entry and you re-greylist the entire internet at once — a self-inflicted delay storm right after a reboot.
Both systems, non-negotiable: never greylist mail you'll reject anyway (restriction ordering, above), and never greylist your own transactional relays. Whitelist your SES, Postmark, and local app-relay IPs explicitly — the login mail your own platform sends must never sit in a greylist queue. If you do inbound forwarding, remember SRS rewrites the envelope sender, which changes Postgrey's triplet; rspamd's body-hash keying is unbothered by it.
Operator checklist
Delay ≤ 60s on Postgrey, or rely purely on the score gate with rspamd.
Whitelist major-provider SPF ranges (_spf.google.com, spf.protection.outlook.com, amazonses.com) *and* your own transactional relays.
rspamd: send yourself a DKIM-signed test message and confirm it is delivered on the first attempt — it must skip greylisting.
Restriction order: the greylist step comes *after* all reject checks, so you never delay a message you'd bounce.
Redis persistence (appendonly yes) enabled so a restart doesn't re-greylist the world.
Measure before declaring victory. Don't guess at your induced latency — read it off the log.
The verdict isn't close. The rspamd module wins on selectivity, on IP-pool resilience, and on running one fewer daemon. If you already run rspamd, delete Postgrey today — you're paying latency for a filter rspamd does better. Postgrey only makes sense on a bare Postfix box with no scanner, and only after you've neutered it with a sub-60-second delay and aggressive provider whitelisting.
Before and after any change, quantify your actual greylist-induced delay straight from mail.log:
bash
# how many messages got greylisted today
grep -c 'Greylisted' /var/log/mail.log
# first-hop latency distribution on accepted mail
grep 'status=sent' /var/log/mail.log \
| grep -oP 'delay=\K[0-9.]+' \
| sort -n | tail -20
If that tail is full of 300+ second values on mail from Google or Microsoft, your greylist is the reason — and now you know which knob fixes it.