deliverability-guard
A sending circuit breaker for outbound email — it watches reputation, bounce, complaint and warmup signals per mailbox and throttles or pauses before a domain burns, and it refuses to trip on statistically meaningless data.
The honest limits
For senders under roughly 1,000 messages/day/provider, this is a leading-indicator and compliance monitor, not a statistically valid complaint-rate breaker. Overclaiming here would make it wrong in production.
That sentence is a credibility asset, not a hedge. It stays here, first, on purpose.
At cold-outbound volume — which is most of the volume this tool is built for — a single complaint moves the observed rate by amounts a fixed-window breaker cannot interpret honestly. This tool models that uncertainty explicitly instead of pretending it isn't there. See docs/statistics.md for the full argument and docs/limits.md for what the underlying data itself can't tell you, no matter how the statistics are done.
The 0.15-of-a-message problem
A sender running 50 emails/day through one mailbox — an entirely ordinary cold-outbound volume. Gmail's hard ceiling is a 0.3% spam rate.
0.3% × 50 = 0.15 messages
You cannot receive 0.15 of a complaint. The only things that can actually happen in a day are 0 complaints (0%) or 1 complaint (2% — 6.7× over the ceiling). There is nothing representable in between. A breaker that watches the raw percentage will fire on that single complaint, because 2% really is greater than 0.3% — and it will be wrong to do so, because one data point at this volume carries almost no information about the mailbox's true underlying rate.
deliverability-guard never looks at the raw percentage. It models the unknown true rate as a Bayesian posterior and trips on the lower bound of a 95% credible interval, not the point estimate — the same one complaint in 50 sends that reads as "2%, over the limit, pause it" to a naive tool reads as "nowhere near enough evidence" here. See docs/statistics.md for the full worked example, and run it yourself:
uv run python examples/demo.py
Quick start (dry-run)
Dry-run is the default everywhere in this project. It is not a separate code path — it's the exact same evaluation logic, with a no-op decorator standing in for the real provider call, so what you see in dry-run is what would actually happen.
git clone https://github.com/katekruger/deliverability-guard
cd deliverability-guard
uv sync
cp .env.example .env # fill in your provider API key
cp config/thresholds.example.yml config/thresholds.yml
from datetime import UTC, datetime
from deliverability_guard.engine.breaker import DEFAULT_LADDER, BreakerStateStore, evaluate
from deliverability_guard.engine.posterior import DEFAULT_PRIOR
from deliverability_guard.providers.base import MailboxRef
from deliverability_guard.providers.instantly import InstantlyDriver
driver = InstantlyDriver(api_key="...") # never contacted while dry_run=True
mailbox = MailboxRef(provider="instantly", mailbox_id="sender@yourdomain.com")
result = evaluate(
driver=driver,
mailbox=mailbox,
sends=50,
complaints=1,
prior=DEFAULT_PRIOR,
thresholds=DEFAULT_LADDER,
state_store=BreakerStateStore(),
dry_run=True, # no code path can pause or throttle without dry_run=False, set on purpose
now=datetime.now(UTC),
)
print(result.verdict) # Verdict.OK -- one complaint in 50 sends isn't evidence of anything yet
There is no CLI yet. The public surface is the engine, providers, signals, and identity modules; examples/demo.py is the fastest way to see it run.
Provider capability matrix
Capability declaration is the whole design of the provider driver interface (providers/base.py): of nine sequencer platforms surveyed, two cannot pause anything at all. An interface that assumed every provider could pause would already be wrong for close to half of what was surveyed. Every driver's pause()/throttle() is always callable and returns an explicit "unsupported" result rather than silently doing nothing when a provider lacks a capability.
| Provider | Status in this repo | Read stats | Pause | Throttle | Bounce webhook |
|---|---|---|---|---|---|
| Instantly | ✅ Implemented (reference driver) | per-mailbox daily | ✅ mailbox or campaign | ❌ no primitive | yes |
| Smartlead | ✅ Implemented (proves the throttle path) | per-campaign | campaign only, not per-mailbox | ✅ per-mailbox daily limit | yes |
| Lemlist | Researched, not yet implemented | activities | idempotent, campaign-level | ❌ | unverified |
| Apollo | Researched, not yet implemented | email stats | list only | ❌ | polling only |
| Outreach | Researched, not yet implemented | yes | likely (unverified) | likely (unverified) | best-in-class |
| Salesloft | Researched, not yet implemented | yes | likely UI-only — no API | ❌ | unverified |
| Amplemarket | Researched, not yet implemented | ❌ | no status-change API at all — app-only | ❌ | ❌ |
| Amazon SES | Researched, not yet implemented | via CloudWatch | account-level (config set) | via config set | via SNS |
| Postmark | Researched, not yet implemented | yes | ❌ no pause primitive | ❌ | yes |
| SendGrid | Researched, not yet implemented | yes | ❌ no pause primitive | ❌ | yes |
Instantly is the reference implementation because it's the only surveyed vendor exposing both per-mailbox daily bounce data and per-mailbox pause — the minimum viable substrate for a circuit breaker. Smartlead's per-mailbox daily-limit endpoint is, in this project's view, the most underrated endpoint in the space: it's the difference between a circuit breaker and a kill switch, which is why it's the second driver built rather than a third pause-only one.
Thresholds
The default ladder (config/thresholds.example.yml, engine/breaker.py), keyed on the posterior's lower confidence bound, never the point estimate:
| State | Threshold | Action |
|---|---|---|
| Warn | ≥ 0.05% | Notify only |
| Throttle | ≥ 0.10% | Reduce daily limit 50% |
| Pause | ≥ 0.20% | Pause the mailbox |
Sourced against:
| Rule | Value | Source |
|---|---|---|
| Gmail spam rate — hard ceiling | 0.30% | |
| Gmail spam rate — recommended target | 0.10% | |
| Gmail spam rate cadence | Calculated daily | Google, via Postmaster's getComplianceStatus SPAM_RATE_HIGH reason (docs/postmaster-verdicts.md) |
| Gmail bulk-sender threshold | >5,000/day to Gmail, per primary domain | |
| Gmail enforcement (Nov 2025+) | SMTP 4.7.31/4.7.32 → escalating to 5.7.x |
Google, via Suped |
| Yahoo spam rate | <0.3%, on inbox-delivered mail | Yahoo Sender Hub |
| Microsoft high-volume threshold | ≥5,000/day to outlook/hotmail/live | Microsoft |
| Microsoft penalty | 550 5.7.515 — outright rejection, not junk-foldering |
Microsoft, via dmarcian |
| SES bounce rate — review / pause | 5% / 10% | AWS |
| SES complaint rate — review / pause | 0.1% / 0.5% | AWS |
Google's 0.30% and Amazon SES's 0.10% are NOT the same measurement, and this project never blends them into one number. Google's denominator is Gmail inbox-delivered, DKIM-authenticated mail to engaged users. SES's is mail to domains that return complaint feedback to SES. The ladder above picks threshold values informed by both, as provider-agnostic policy points — never as a claim that the two rates are directly comparable. 0.30% is a terminal threshold: by the time evidence is strong enough to be confident it's been crossed, the damage is already done, because complaint data itself lags 24h–3 days behind send. 0.10% is the natural amber line instead — simultaneously Google's own recommended target and SES's own review trigger, two independent providers converging on the same number as "you should be worried now." See engine/breaker.py's module docstring for the full rationale.
What this cannot see
- IP reputation. For the dominant cold-outbound topology (Google Workspace / Microsoft 365 shared sending pools), IP reputation is both unobservable to a sender and not theirs to fix. Google itself removed the
DOMAIN_REPUTATIONandIP_REPUTATIONmetrics from Postmaster Tools v2 — there is no metric to read even if this project wanted one. This isn't a gap to be filled later; it's a design constraint, stated inBUILD-PLAN.md. - Inbox placement. This tool has no visibility into whether a delivered message actually landed in the primary inbox versus a spam or promotions folder — it can see bounces, complaints, and compliance verdicts, not placement.
- Postmaster's own confidence bounds — removed in v2. Google's v1 Postmaster API exposed
userReportedSpamRatioLowerBound/UpperBoundalongside the point estimate. v2 dropped them entirely (confirmed against the live discovery document, docs/postmaster-verdicts.md) — a real regression this project doesn't have a way to work around, only to be honest about: every Postmaster-sourced rate is treated as a bare point estimate, run through the same posterior machinery as any other rate. - Which campaign caused a spike, without the identity scheme adopted first. Postmaster reports a domain-day scalar; the sequencer reports per-message events; there is no join key between them by default.
identity/feedback_id.pyandidentity/subdomain_advisor.pyfix this — but only for mail sent after they're adopted, and only if the underlying sending architecture actually follows the scheme. Seedocs/limits.md. - Warmup adherence. Not implemented in v0.1. When it lands, it will ship as explicitly-labeled vendor-consensus heuristics, not a standard — there is no RFC or independent research behind published warmup curves, and presenting folklore as authoritative would be this project's fastest way to lose credibility.
License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file deliverability_guard-0.1.0.tar.gz.
File metadata
- Download URL: deliverability_guard-0.1.0.tar.gz
- Upload date:
- Size: 236.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
846816ef0c4e53ba4a6c06ed0a5cfa55790aa2be6443e9f7c578f84b49ccbfaf
|
|
| MD5 |
fc2b7f0b7f8ec61db0de6427c58947c6
|
|
| BLAKE2b-256 |
2969b9b6677f829a55a3ed7e97d7426f11fbaec3d2a2a7fd532737096b294bac
|
Provenance
The following attestation bundles were made for deliverability_guard-0.1.0.tar.gz:
Publisher:
release.yml on katekruger/deliverability-guard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
deliverability_guard-0.1.0.tar.gz -
Subject digest:
846816ef0c4e53ba4a6c06ed0a5cfa55790aa2be6443e9f7c578f84b49ccbfaf - Sigstore transparency entry: 2641951668
- Sigstore integration time:
-
Permalink:
katekruger/deliverability-guard@c2a642a3525091d9632152e7c21c0fa65d5ee852 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/katekruger
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c2a642a3525091d9632152e7c21c0fa65d5ee852 -
Trigger Event:
push
-
Statement type:
File details
Details for the file deliverability_guard-0.1.0-py3-none-any.whl.
File metadata
- Download URL: deliverability_guard-0.1.0-py3-none-any.whl
- Upload date:
- Size: 51.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1041b1f5e17a8176fc764275001cfdebf0b6157eae83d0079478933feeec2df1
|
|
| MD5 |
823fde488fa82b701d8b21720e7f6e4d
|
|
| BLAKE2b-256 |
b3747b697472bd47f9da3b3b6789a20dff101bc056f3fd2250156b42e72fbb47
|
Provenance
The following attestation bundles were made for deliverability_guard-0.1.0-py3-none-any.whl:
Publisher:
release.yml on katekruger/deliverability-guard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
deliverability_guard-0.1.0-py3-none-any.whl -
Subject digest:
1041b1f5e17a8176fc764275001cfdebf0b6157eae83d0079478933feeec2df1 - Sigstore transparency entry: 2641951683
- Sigstore integration time:
-
Permalink:
katekruger/deliverability-guard@c2a642a3525091d9632152e7c21c0fa65d5ee852 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/katekruger
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c2a642a3525091d9632152e7c21c0fa65d5ee852 -
Trigger Event:
push
-
Statement type: