Merge pull request #63 from warmbly/feature/coming-soon-banner
Launch prep: coming-soon banner, self-host warmup notice, trial/free-tier copy, and the SEO blog cluster
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 666 KiB |
@@ -9,8 +9,8 @@
|
||||
* web-app-manifest-192x192.png
|
||||
* web-app-manifest-512x512.png
|
||||
*
|
||||
* The 1200x630 OG / Twitter card (public/og-image.png) is generated
|
||||
* separately from scripts/og-image.html — see scripts/render-og.py.
|
||||
* The 1200x600 OG / Twitter card (public/og-image.jpg) is exported from the
|
||||
* Figma Branding file (the "Social" frame), not generated here.
|
||||
*
|
||||
* Run with: node scripts/gen-icons.mjs
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
/**
|
||||
* Launch announcement bar, rendered site-wide above the header.
|
||||
*
|
||||
* Warmbly Cloud (hosted sending + the shared warmup pool) is not live yet.
|
||||
* The whole platform is open source and self-hostable today; the cloud
|
||||
* waitlist lives at app.warmbly.com. Keep this until cloud launches, then
|
||||
* remove it from the Layout.
|
||||
*
|
||||
* Built from the site's own hero vocabulary: a rich single-family sky gradient
|
||||
* with a soft light source, a glass CTA chip (same as the hero badges), a
|
||||
* typographic mid-dot separator, tight heading tracking. Not dismissible.
|
||||
*/
|
||||
---
|
||||
<aside
|
||||
class="relative z-40 w-full text-white"
|
||||
style="
|
||||
background:
|
||||
radial-gradient(135% 240% at 15% -60%, rgba(186,230,253,0.55) 0%, rgba(56,189,248,0) 48%),
|
||||
linear-gradient(180deg, #0ea5e9 0%, #0284c7 100%);
|
||||
box-shadow: inset 0 1px 0 0 rgba(255,255,255,0.16), inset 0 -1px 0 0 rgba(2,33,54,0.22);
|
||||
"
|
||||
aria-label="Launch status"
|
||||
>
|
||||
<div class="container-page flex h-12 items-center justify-center gap-x-3.5 sm:gap-x-5 text-[13px] sm:text-[13.5px]">
|
||||
<p class="min-w-0 truncate leading-none">
|
||||
<span class="font-semibold tracking-[-0.012em] text-white">Warmbly Cloud is coming soon</span>
|
||||
<span class="hidden md:inline text-white/40"> · </span>
|
||||
<span class="hidden md:inline text-white/75">open source and self-hostable today</span>
|
||||
</p>
|
||||
|
||||
<a
|
||||
href="https://app.warmbly.com"
|
||||
class="group shrink-0 inline-flex items-center gap-1.5 h-8 pl-4 pr-3 rounded-full bg-white/15 ring-1 ring-inset ring-white/30 text-[12.5px] font-semibold text-white backdrop-blur-sm shadow-[0_2px_10px_-4px_rgba(2,33,54,0.5)] hover:bg-white/25 hover:ring-white/45 transition-colors"
|
||||
>
|
||||
<span class="hidden sm:inline">Join the waitlist</span>
|
||||
<span class="sm:hidden">Waitlist</span>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true" class="text-white/90 transition-transform group-hover:translate-x-0.5">
|
||||
<path d="M3.5 3l3 3-3 3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -41,7 +41,7 @@ const contribute = [
|
||||
const selfhost = [
|
||||
'Run on your own infra: Postgres, Redis, Kafka, S3, KMS',
|
||||
'One make up command boots the whole stack',
|
||||
'Or skip the ops and use our managed cloud',
|
||||
'Cloud, with the shared warmup pool, is coming soon',
|
||||
'BYOK encryption and SSO on enterprise',
|
||||
];
|
||||
const links = [
|
||||
@@ -196,9 +196,9 @@ const links = [
|
||||
))}
|
||||
</ul>
|
||||
<p class="mt-4 text-[12px] text-white/60 leading-relaxed">
|
||||
Heads up: shared warmup pools only work with a large, vetted base of mailboxes, which a single
|
||||
self-hosted instance usually will not have, so the shared pool is really only effective on our cloud.
|
||||
Self-hosting still gives you the full sending, inbox and CRM stack.
|
||||
One caveat: the shared warmup pool runs on our cloud. It only works with a large base of vetted
|
||||
mailboxes sending to each other, which a single self-hosted instance starts without. Self-hosting
|
||||
still gives you the full sending, inbox, CRM and API stack. The pool is the one part that needs the cloud.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
/**
|
||||
* "Deep dives" block for a pillar page: links down to the best blog posts in
|
||||
* its topic cluster, so the pillar-to-spoke links mirror the spoke-to-pillar
|
||||
* links the posts already carry. Keeps the cluster reciprocal.
|
||||
*/
|
||||
import Icon from './Icon.astro';
|
||||
|
||||
interface Item {
|
||||
href: string;
|
||||
title: string;
|
||||
kicker?: string;
|
||||
}
|
||||
interface Props {
|
||||
title?: string;
|
||||
items: Item[];
|
||||
}
|
||||
const { title = 'From the blog', items } = Astro.props;
|
||||
---
|
||||
<section class="border-t border-[color:var(--border)] py-16 md:py-20">
|
||||
<div class="container-page">
|
||||
<div class="flex items-end justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Deep dives</div>
|
||||
<h2 class="text-[26px] md:text-[34px] font-semibold tracking-[-0.025em] text-heading leading-[1.08]">{title}</h2>
|
||||
</div>
|
||||
<a href="/blog/" class="hidden sm:inline-flex items-center gap-1.5 text-[13px] font-medium text-[#0369a1] hover:text-[#075985] shrink-0">
|
||||
All posts <Icon name="arrowRight" size={12} />
|
||||
</a>
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-3 gap-3">
|
||||
{items.map((it) => (
|
||||
<a href={it.href} class="group flex flex-col p-5 rounded-[12px] bg-white ring-1 ring-[color:var(--border)] hover:ring-[#0284c7]/40 transition-all min-h-[132px]">
|
||||
{it.kicker && <div class="text-[10.5px] uppercase tracking-[0.16em] font-mono text-muted-foreground mb-2">{it.kicker}</div>}
|
||||
<div class="text-[15px] font-semibold text-heading group-hover:text-[#0369a1] transition-colors leading-snug">{it.title}</div>
|
||||
<div class="mt-auto pt-4 inline-flex items-center gap-1 text-[12px] font-medium text-foreground/45 group-hover:text-[#0369a1] transition-colors">
|
||||
Read <Icon name="arrowRight" size={11} />
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
/**
|
||||
* Reusable notice: the shared warmup pool is a cloud feature, and a single
|
||||
* self-hosted instance starts with an empty pool. Drop it on any light-
|
||||
* background page where a reader might assume self-hosting gives them a
|
||||
* working warmup pool (warmup, deliverability, open-source sections).
|
||||
*
|
||||
* The numbers and behaviour match the warmup pool model in the repo
|
||||
* (internal/repository/pg_warmup.go, migration 000010).
|
||||
*/
|
||||
import Icon from './Icon.astro';
|
||||
|
||||
interface Props {
|
||||
/** extra margin/utility classes on the outer card */
|
||||
class?: string;
|
||||
/** show the cloud waitlist link (default true) */
|
||||
showWaitlist?: boolean;
|
||||
}
|
||||
const { class: className = '', showWaitlist = true } = Astro.props;
|
||||
---
|
||||
<aside class={`rounded-[14px] ring-1 ring-[color:var(--border)] bg-white overflow-hidden shadow-[var(--shadow-sm)] ${className}`}>
|
||||
<div class="flex items-start gap-4 p-5 md:p-6">
|
||||
<span class="shrink-0 mt-0.5 w-9 h-9 rounded-lg bg-sky-50 ring-1 ring-sky-100 grid place-items-center text-[color:var(--sky-6)]">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect width="20" height="8" x="2" y="2" rx="2"/><rect width="20" height="8" x="2" y="14" rx="2"/><path d="M6 6h.01M6 18h.01"/></svg>
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<div class="text-[10.5px] uppercase tracking-[0.18em] font-mono text-muted-foreground mb-1.5">Self-hosting and the warmup pool</div>
|
||||
<p class="text-[14px] text-foreground/80 leading-relaxed">
|
||||
The shared warmup pool runs on Warmbly Cloud. It works because a large base of vetted mailboxes send and reply to each other, and any mailbox that starts landing in spam gets quarantined. A self-hosted instance starts with an empty pool, so warmup has no healthy partners to exchange mail with. To run your own pool you need many real, monitored mailboxes and the ongoing work to keep them clean.
|
||||
</p>
|
||||
<p class="mt-2.5 text-[14px] text-foreground/80 leading-relaxed">
|
||||
Everything else runs on your own infrastructure: sending, sequences, the unified inbox, CRM, tracking, and the API.
|
||||
</p>
|
||||
{showWaitlist && (
|
||||
<a href="https://app.warmbly.com" class="mt-3 inline-flex items-center gap-1.5 text-[13px] font-medium text-[#0369a1] hover:text-[#075985]">
|
||||
Want the pool without running the mailboxes? Join the cloud waitlist
|
||||
<Icon name="arrowRight" size={12} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: 'What complaint, bounce, and spam rate gets you throttled?'
|
||||
description: 'The numbers that actually trigger enforcement: Google spam under 0.10%, SES complaint under 0.1% and bounce under 5%. Plus the stricter bands we quarantine at.'
|
||||
pubDate: 2026-06-16
|
||||
cover: '/blog/complaint-rate-that-gets-you-throttled.webp'
|
||||
tags: ['deliverability', 'policy']
|
||||
---
|
||||
import Callout from '../../components/blog/Callout.astro';
|
||||
|
||||
Google wants your user-reported spam rate under 0.10% and says never reach 0.30%. Amazon SES wants complaint rate under 0.1% and bounce rate under 5%, and it starts account reviews at those numbers. Below is every threshold that triggers enforcement, anchored to the provider that enforces it, then the stricter bands Warmbly uses to pull a mailbox before providers see the problem.
|
||||
|
||||
## The thresholds that trigger provider enforcement, in one table
|
||||
|
||||
Three separate failures get you throttled: complaints, bounces, and spam-folder placement. Providers publish numbers for the first two. Here's what each one does when you cross it.
|
||||
|
||||
| Signal | Named threshold | What triggers it | Consequence |
|
||||
| --- | --- | --- | --- |
|
||||
| User-reported spam (Google) | under 0.10%, never 0.30% | recipients hit "report spam" | filtering, then rejection near 0.30% |
|
||||
| Complaint rate (Amazon SES) | under 0.1% | complaint feedback loops | account review around 0.1%, possible pause around 0.5% |
|
||||
| Bounce rate (Amazon SES) | under 5% | invalid or rejecting recipients | review at 5%, possible pause at 10% |
|
||||
|
||||
The two providers measure slightly different things. Google publishes a spam rate, SES publishes a complaint rate. The practical target is the same: keep reported spam and complaints under one tenth of one percent, and keep hard bounces well under 5%.
|
||||
|
||||
## Google: spam rate under 0.10%, never 0.30%
|
||||
|
||||
Google's bulk sender guidance gives one hard number for reputation: keep your user-reported spam rate under 0.10%, and never let it reach 0.30%. The rate is measured in Google Postmaster Tools against messages Gmail can identify as yours, which is another reason [SPF, DKIM, and DMARC alignment](/blog/spf-dkim-dmarc-alignment-cold-email/) matter. Without alignment, Google can't attribute your good behavior to you.
|
||||
|
||||
The same guidance requires SPF, DKIM, and DMARC with alignment to the visible From domain, plus one-click unsubscribe on bulk and marketing mail. We walk through the full requirement list in [what Google's bulk sender rules ask of cold emailers](/blog/reading-the-bulk-sender-rules/). The number to remember here is 0.10%, because it's the one you can blow through with a single bad campaign.
|
||||
|
||||
## Amazon SES: complaint under 0.1%, bounce under 5% for review
|
||||
|
||||
If you send through Amazon SES, the enforcement is explicit and automated. SES says to keep complaint rate under 0.1%. Around 0.1% it puts the account under review, and around 0.5% it may pause sending. For bounces, SES wants you under 5%. At 5% the account can go under review, and at 10% SES may pause sending outright.
|
||||
|
||||
SES enforces these at the account level, not per mailbox, so one bad list can freeze every mailbox on the account. That's a reason to spread sending across mailboxes and domains, which is also the [right way to scale volume](/blog/how-many-cold-emails-per-day-per-mailbox/).
|
||||
|
||||
## What 0.10% means at 50 emails a day per mailbox
|
||||
|
||||
The percentage sounds forgiving until you do the division at cold-email volume. Warmbly's default cap is 50 campaign emails per mailbox per day. That's about 1,500 sends a month per mailbox.
|
||||
|
||||
<Callout title="The math that should change how you write">
|
||||
At 50 emails a day, a mailbox sends about 1,500 a month. Staying under Google's 0.10% spam rate means at most one complaint per mailbox per month. One person out of 1,500 hitting "report spam" puts that mailbox at the line. There's no filtering trick downstream of that number: the message has to be relevant enough that nearly nobody who gets it is angry about getting it.
|
||||
</Callout>
|
||||
|
||||
One complaint per mailbox per month is the whole budget. It reframes what "good targeting" means. Every deliverability tactic (warmup, authentication, spacing) sits downstream of writing mail the person on the other end doesn't resent. If your list quality forces you above one complaint per 1,500 sends, no configuration change saves you.
|
||||
|
||||
## Why we quarantine stricter than the providers do
|
||||
|
||||
Providers act at their thresholds to protect their own users. A shared warmup pool has to act earlier, because a mailbox that reaches the provider's line has already spent weeks degrading the pool it sends into. Warmbly's health bands sit below the provider numbers on purpose.
|
||||
|
||||
| Band | Trigger | What happens |
|
||||
| --- | --- | --- |
|
||||
| Watch | 10% spam placement | lower volume, wider spacing |
|
||||
| Quarantine | 20% placement, or 0.10% complaints, or 5% bounce | out of the shared pool, 7 days |
|
||||
| Block | 40% placement, or 0.30% complaints, or 10% bounce | out 30 days, manual review |
|
||||
|
||||
Notice where the block band lands. 0.30% complaints and 10% bounce are the exact points Google and SES escalate to rejection or pause. By the time a mailbox hits our block band, it's already at the provider's cliff. Quarantine fires at 0.10% complaints and 5% bounce, one full band earlier, so the mailbox leaves the pool while its reputation can still recover. We make the case for acting early in [quarantine early: how we protect shared warmup pools](/blog/quarantine-early/).
|
||||
|
||||
None of these bands fire on one bad data point. Placement is judged on at least 20 warmup deliveries across 7 days, so a single message in spam on a Tuesday quarantines nobody. Re-entry isn't automatic when the 7 days expire, either. A mailbox has to requalify with authentication intact, no new complaint or bounce spike, clean warmup-token behavior, and placement back under 10% on a fresh probation sample, [re-warming from 5 to 10 a day](/blog/getting-a-mailbox-back-into-warmup-pool/).
|
||||
|
||||
## Bounce, complaint, and placement are different failures, measure them apart
|
||||
|
||||
The most common mistake is collapsing these into one "deliverability score." They have different causes and different fixes, so a single number hides what's wrong.
|
||||
|
||||
- Bounce rate is a list-quality problem. Hard bounces mean the address is dead or rejecting. The fix is list hygiene and verification, not warmup. SES reviews at 5%.
|
||||
- Complaint rate is a content and targeting problem. The message reached the inbox and the person didn't want it. The fix is relevance and suppression on any opt-out. Google's line is 0.10%, SES's is 0.1%.
|
||||
- Spam-folder placement is a reputation and authentication problem. The message was accepted but filed in junk. Providers don't hand you this number directly, which is why seed and warmup observations exist to estimate it. Placement is [not the same as your delivery rate](/blog/delivery-rate-is-not-inbox-placement/): a message can "deliver" and still land in spam.
|
||||
|
||||
Measure them apart because the response differs. A bounce spike means stop and clean the list. A complaint spike means fix the message or the audience. A placement drop with clean bounces and complaints usually points at authentication or a warmup gap. Warmbly's [analytics](/analytics/) track the three separately so you react to the real failure, and the broader [deliverability](/deliverability/) picture stays readable instead of averaged into one figure.
|
||||
|
||||
If you want the source numbers, read Google's bulk sender guidance and Amazon SES's sending-review documentation directly. Our own [deliverability guide](https://docs.warmbly.com/learn/deliverability/) collects the thresholds in one place, and Warmbly is open source if you want to read exactly how the bands are wired ([github.com/warmbly/warmbly](https://github.com/warmbly/warmbly)). The one number to carry out of here: one complaint per mailbox per month is your entire budget at 50 sends a day. Write like that's true, because to Gmail it is.
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: 'Delivery rate is not inbox placement, and the gap is where your emails die'
|
||||
description: 'A 99% delivery rate can still mean the spam folder. What delivered actually means, why it is not placement, and how to tell where your cold email lands.'
|
||||
pubDate: 2026-06-27
|
||||
cover: '/blog/delivery-rate-is-not-inbox-placement.webp'
|
||||
tags: ['deliverability']
|
||||
---
|
||||
import Callout from '../../components/blog/Callout.astro';
|
||||
|
||||
Your ESP says 99% delivered. Your reply count says something else. Both can be true, because delivered is an accounting term, not a placement fact. This post explains the gap between the two numbers, why open rate can't measure it, and which signals can.
|
||||
|
||||
## Answer first: delivered means accepted, not seen
|
||||
|
||||
Delivered means the receiving mail server accepted the message at the SMTP handshake and didn't bounce it. That's all it means. The message could be in the primary inbox, in Promotions, in the spam folder, or filtered into a folder nobody reads. All of those count as delivered. None of them are the same thing.
|
||||
|
||||
Inbox placement rate is the fraction of accepted mail that reaches the primary inbox where a human might read it. A mailbox can post a 99% delivery rate and a 40% inbox placement rate at the same time. That 59-point gap is invisible on the dashboard your ESP hands you, and it's where cold email dies: accepted, filed in spam, never opened, never answered.
|
||||
|
||||
If your delivered number looks great and your replies are near zero, you're probably reading the wrong number.
|
||||
|
||||
## What your ESP's 99% delivery number is really counting
|
||||
|
||||
The 99% counts SMTP acceptances minus bounces. Google, Amazon SES, and Microsoft all accept mail first and sort it second. A hard bounce (no such mailbox, domain doesn't exist) fails the handshake and drops your delivery rate. Spam filing happens after acceptance, so it never touches that number.
|
||||
|
||||
Here's the same send counted two ways.
|
||||
|
||||
| Metric | What it counts | Where it's decided |
|
||||
| --- | --- | --- |
|
||||
| Delivery rate | accepted minus bounced | at the SMTP handshake |
|
||||
| Inbox placement rate | accepted mail reaching the primary inbox | after acceptance, by the filter |
|
||||
| Bounce rate | rejected at handshake | at the SMTP handshake |
|
||||
|
||||
Amazon SES tells senders to keep bounce rate under 5% (account review at 5%, possible pause at 10%). That's a delivery-side number, and it matters. But low bounces say nothing about whether the accepted mail landed in the inbox or in junk. Two mailboxes with identical 0.5% bounce rates can have wildly different placement, and only one of them gets replies.
|
||||
|
||||
## Why open rate can't fix the gap (Apple MPP and image proxies)
|
||||
|
||||
The instinct is to fill the gap with open rate. Don't. Open rate is measured with a tracking pixel, and the two largest mail clients load that pixel without a human involved.
|
||||
|
||||
Apple Mail Privacy Protection pre-fetches images the moment mail arrives, before the recipient opens anything. Apple loads your pixel for messages the user never sees, including messages sitting in spam. Gmail proxies and caches every image through its own servers, so a Gmail open can fire from Google's infrastructure on a schedule you don't control. Add security scanners and link-preview bots that fetch images to inspect them, and a large share of your opens are machines.
|
||||
|
||||
An inflated open rate can't tell you about placement, because a pixel in the spam folder still loads. You can post a 45% open rate and a 2% reply rate, and the honest read is: the pixel is firing, the humans aren't.
|
||||
|
||||
## The signals that indicate placement
|
||||
|
||||
Placement leaves fingerprints that don't depend on a pixel. Three are worth watching.
|
||||
|
||||
Warmup seed data is the most direct. In a warmup pool, participating mailboxes send to and reply to each other, so you control both ends and can check which folder each message landed in. Warmbly pools draw partner mailboxes from the same pool, exclude recent partners for 7 days, and judge spam-folder placement on at least 20 deliveries over 7 days before acting. That folder-level truth is the closest thing to a placement measurement you can get without paid seed lists.
|
||||
|
||||
Replies are the signal a machine can't fake. A reply means the message reached a human, that human read it, and they cared enough to type. Reply rate tends to track inbox placement more honestly than any pixel, because Apple's servers don't answer your questions and Gmail's proxy doesn't ask for a demo.
|
||||
|
||||
Complaint rate is the negative signal, and providers publish the thresholds. Google's bulk-sender guidance says keep user-reported spam under 0.10% and never reach 0.30%. Amazon SES says keep complaint rate under 0.1% (review around 0.1%, possible pause around 0.5%). A rising complaint rate means your mail is reaching inboxes and people are rejecting it, which is a targeting or content problem, not a placement one. Both matter, and they fail in different directions.
|
||||
|
||||
## How Warmbly reads placement without trusting the open pixel
|
||||
|
||||
Warmbly runs its own open and click tracking service, so it can separate the machine loads from the human ones. It classifies prefetches (Apple MPP), the Gmail image proxy, and known scanner user agents, and it deliberately does not filter Gmail's image proxy, because that proxy is the only open signal Gmail exposes. Filter it and you go blind to every Gmail open. Keep it and you know the load is Gmail-shaped, so you can weight it accordingly. Either way, the raw pixel count never gets treated as a human read.
|
||||
|
||||
<Callout title="Why the Gmail proxy stays unfiltered">
|
||||
Apple MPP and scanner fetches get flagged as machine loads. Gmail's proxy is the exception we keep on purpose: it's the single open signal Gmail gives up, so filtering it would erase all visibility into Gmail opens. We surface it and weight it, rather than trust it as a human open or throw it away.
|
||||
</Callout>
|
||||
|
||||
The measurement that carries weight comes from the warmup pool, where both ends are instrumented. Each warmup message carries a signed HMAC verification token in a footer ref line, validated on classification for signature, a 48-hour expiry, single use, pool membership, and a sender/recipient match. That lets Warmbly confirm a given message is a real pool send and see which folder it reached. That folder-level read feeds the health bands: Watch at 10% spam placement, Quarantine at 20% placement or 0.10% complaints or 5% bounces. It's placement data, not pixel data. You can see the reasoning behind [how deliverability signals drive pool decisions](/deliverability/) and [what the dashboard surfaces](/analytics/), and the tracking service is open source at [github.com/warmbly/warmbly](https://github.com/warmbly/warmbly) under Apache 2.0.
|
||||
|
||||
## A quick self-audit when delivered looks great but replies are zero
|
||||
|
||||
Run this before you blame your copy. It takes about 15 minutes and points at the real failure.
|
||||
|
||||
1. Send a test to three seed mailboxes you own on Gmail, Outlook, and one more provider, from the sending mailbox in question. Check which folder each one lands in by hand. Delivered but in spam is the exact condition this whole post is about, and manual folder checks catch it when the dashboard won't.
|
||||
2. Compare reply rate to open rate on your last 200 sends. A high open rate with near-zero replies is the Apple-and-Gmail-pixel pattern, not a sign of interest.
|
||||
3. Check authentication alignment, not just pass/fail. SPF has a 10 DNS-lookup limit, and DMARC needs SPF or DKIM to align with your visible From domain. Both can pass while DMARC alignment fails, and Google requires alignment for bulk mail. See [SPF, DKIM, DMARC alignment for cold email](/blog/spf-dkim-dmarc-alignment-cold-email/).
|
||||
4. Read your bounce rate and complaint rate as separate numbers. Bounces are a list problem, complaints are a targeting problem, and [the complaint rate that gets you throttled](/blog/complaint-rate-that-gets-you-throttled/) starts well below where a provider pauses you.
|
||||
5. If placement is bad on a mailbox, slow it down before you push more volume through it. Warmbly's default cold cap is 50 emails per mailbox per day with a minimum 600 seconds between sends, and a brand-new mailbox typically needs 3 to 6 weeks of warmup to reach a stable state. More volume through a filtered mailbox digs the hole deeper.
|
||||
|
||||
The number to trust is the one a machine can't inflate. Watch folder placement in a pool where you control both ends, watch reply rate, watch complaint rate against Google's 0.10% and Amazon SES's 0.1% lines. When those three agree, your delivered number finally means what you thought it meant.
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: 'Getting a burned mailbox back: a reputation recovery playbook'
|
||||
description: 'A step-by-step recovery ramp for a mailbox that has been quarantined or blacklisted: confirm listings, stop the bleeding, fix the root cause, re-warm at 5 to 10/day.'
|
||||
pubDate: 2026-06-19
|
||||
cover: '/blog/getting-a-mailbox-back-into-warmup-pool.webp'
|
||||
tags: ['warmup', 'deliverability']
|
||||
---
|
||||
import Callout from '../../components/blog/Callout.astro';
|
||||
|
||||
A burned mailbox recovers by fixing the cause, sitting silent while the damage ages out, then re-warming from 5 to 10 emails a day once placement is back under 10% on a fresh sample. There's no reset button, and requesting delisting doesn't reset reputation. Here's the sequence that tends to work, and the numbers that tell you where you are.
|
||||
|
||||
## First, confirm what's actually wrong
|
||||
|
||||
Before you fix anything, name the failure. "Everything went to spam" is four different problems, and each has a different way out.
|
||||
|
||||
Check these in order:
|
||||
|
||||
- DNSBL listings. Query your sending IP and domain against Spamhaus, Barracuda, and SURBL. A listing tells you which list and usually why (snowshoe, dictionary attack, honeypot hit).
|
||||
- Complaint rate. Amazon SES puts an account under review around 0.1% and may pause sending around 0.5%. Google's bulk-sender guidance says keep user-reported spam under 0.10% and never reach 0.30%. Above 0.10% is the fire.
|
||||
- Bounce rate. SES flags at 5% and may pause at 10%. A high hard-bounce rate points at list quality, not content.
|
||||
- Authentication. Run your From domain through an SPF, DKIM, and DMARC check. SPF has a 10 DNS-lookup limit, and DMARC needs SPF or DKIM to align with the visible From domain, so both can pass while DMARC alignment fails.
|
||||
- Warmup token behavior, if you're on a pool. Warmbly quarantines a mailbox at 3 or more invalid warmup tokens in 24 hours, which usually means someone's gaming the pool or your mailbox is misclassifying.
|
||||
|
||||
Keep these separate. A bounce problem is a list problem. A complaint problem is a content or targeting problem. Collapsing them into one "reputation" number hides the repair. Our [deliverability](/deliverability/) page walks the same signal-by-signal split.
|
||||
|
||||
## Stop sending from the affected mailbox and infrastructure
|
||||
|
||||
Once you know it's burned, stop. Every message you send while the mailbox lands in spam adds another negative data point that has to age out before you recover.
|
||||
|
||||
Stopping means the mailbox and the infrastructure around it. In Warmbly, email accounts are assigned to specific workers, and each worker runs on one machine with its own IP. Planned worker volume equals the sum of its mailboxes' daily caps, so no single IP becomes a concentration point. If a mailbox is burned, check what else shares its worker and its domain. A shared IP that carried a spam-flagged sender may need the same pause. A burned domain affects every mailbox on it, not only the one that tripped the alarm.
|
||||
|
||||
Pause the campaigns. Pull the mailbox out of any warmup pool. If it was in a shared pool, its bad placement was already damaging its partners, which is why Warmbly quarantines at 20% placement (over at least 20 deliveries in 7 days) rather than waiting.
|
||||
|
||||
## Fix the root cause before you request anything
|
||||
|
||||
Delisting or re-warming a mailbox whose underlying problem is still live puts you right back where you started. Fix the cause first.
|
||||
|
||||
Match the fix to the signal you found:
|
||||
|
||||
| Signal | Likely cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| DNSBL listing | Compromised account, bad list, honeypot | Secure the account, purge the list segment, then request delisting |
|
||||
| Complaints over 0.10% | Content, targeting, no easy unsubscribe | Tighten targeting, add one-click unsubscribe, cut frequency |
|
||||
| Bounces over 5% | Stale or unverified list | Verify addresses, remove hard bounces, stop importing scraped lists |
|
||||
| DMARC alignment fails | SPF/DKIM pass but don't align to From | Align the visible From domain with a signing or SPF-authorized domain |
|
||||
| Invalid warmup tokens | Pool gaming or misclassification | Investigate the mailbox before requalifying |
|
||||
|
||||
Google requires SPF, DKIM, and DMARC with alignment plus one-click unsubscribe for bulk and marketing mail. If any of that is missing, add it now. It's the entry requirement, and you can't recover reputation while failing it.
|
||||
|
||||
## Requesting delisting, and why it doesn't reset reputation
|
||||
|
||||
If you're on a DNSBL, most operators (Spamhaus, Barracuda) have a removal request form. Fill it out honestly, state what you fixed, and submit once. Repeated requests without a real fix tend to slow things down, not speed them up.
|
||||
|
||||
Here's the part people miss. Delisting removes a specific blacklist entry. It doesn't reset the reputation that mailbox providers (Google, Microsoft, Apple) track independently. Those providers score your domain and IP on their own history: complaint rate, engagement, spam-folder placement, how long you've sent from that identity. A clean DNSBL and a healthy provider reputation are two different things. You can be off every blacklist and still land in Gmail's spam folder because Gmail's own history of your sender is still bad.
|
||||
|
||||
Treat delisting as one item on the checklist, not the finish line. Provider reputation recovers by sending good mail slowly, not by filling out a form.
|
||||
|
||||
## The re-entry checklist: what has to be true before re-warming
|
||||
|
||||
A mailbox doesn't go back into rotation because time passed. It requalifies. These are the conditions Warmbly requires before a quarantined mailbox re-enters the shared pool, and they work as a general re-entry bar too:
|
||||
|
||||
- Authentication intact: SPF within the 10-lookup limit, DKIM signing, DMARC aligned to the visible From domain.
|
||||
- No recent complaint or bounce spikes. Complaints back under 0.10%, bounces under 5%.
|
||||
- Clean token behavior: no recent invalid or forged warmup tokens.
|
||||
- Spam-folder placement back under 10% on a fresh probation sample, not on the old poisoned data.
|
||||
- Willingness to re-warm from low volume, 5 to 10 a day, instead of resuming at the old cap.
|
||||
|
||||
<Callout title="Placement, not opens" variant="warn">
|
||||
Judge re-entry on where mail lands, not on open rate. Apple Mail Privacy Protection pre-fetches images, so it inflates opens whether or not a human looked. Gmail proxies and caches images too. A high open rate on a burned mailbox is often Apple and Gmail machinery, not recovery. Use seed placement or your pool's spam-folder rate instead.
|
||||
</Callout>
|
||||
|
||||
## The recovery ramp: back in at 5 to 10 a day on a probation sample
|
||||
|
||||
When the checklist passes, re-warm like the mailbox is new, because to the providers it partly is. Start at 5 to 10 emails a day. That's below Warmbly's normal warmup start of 10/day, on purpose, because a recovering mailbox carries negative history a fresh one doesn't.
|
||||
|
||||
Run a probation sample first. Send a small, low-risk batch (warmup traffic or a tightly targeted, high-engagement segment) and watch placement before you scale. If placement holds under 10%, ramp gradually, roughly +1 a day, the way our [warmup](/warmup/) ramps toward its 40/day ceiling. If placement climbs back toward the Watch band at 10%, cut volume and add spacing. Warmbly keeps at least 600 seconds (10 minutes) between sends from one mailbox during normal sending, and spacing matters more during recovery.
|
||||
|
||||
Keep the signals separate on the way back up. If bounces are clean but complaints creep, that's a targeting fix, not a volume fix. Our post on [quarantining early](/blog/quarantine-early/) covers why one blended health score hides the thing you need to see during a ramp.
|
||||
|
||||
## Recovery timelines: reading the weeks
|
||||
|
||||
Recovery time depends on what's damaged. IP reputation tends to recover faster than domain reputation, because providers weight the domain more heavily and it carries more history. No one outside the provider controls the score, so treat any timeline as an estimate:
|
||||
|
||||
- IP reputation: often a few weeks of consistent, low-volume, low-complaint sending before placement stabilizes.
|
||||
- Domain reputation: often longer, sometimes much longer if the domain took repeated complaint or bounce spikes.
|
||||
|
||||
For comparison, a brand-new Warmbly mailbox typically takes 3 to 6 weeks to reach a stable warmed state. A recovering mailbox can run longer, because it's climbing out of a hole a new mailbox never dug.
|
||||
|
||||
These aren't countdowns. Two weeks of sending bad mail recovers nothing. Two weeks of clean, spaced, low-volume sending with placement trending under 10% is what moves the number. If the timeline stalls, the cause isn't fixed. Go back to the top and re-check the signals one at a time.
|
||||
|
||||
Warmbly is open source under Apache 2.0 ([github.com/warmbly/warmbly](https://github.com/warmbly/warmbly)), and the recovery bands here are the same ones the pool enforces. The full walkthrough lives at [docs.warmbly.com/learn](https://docs.warmbly.com/learn/reputation-recovery/).
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: 'How email warmup actually works, ramp curve and all'
|
||||
description: 'Warmup ramps a mailbox from 10/day, +1/day, to a 40/day ceiling over a few weeks. Here is the real mechanism: pools, partner selection, tokens, and why we cap it.'
|
||||
pubDate: 2026-06-30
|
||||
cover: '/blog/how-email-warmup-actually-works.webp'
|
||||
tags: ['warmup', 'deliverability']
|
||||
---
|
||||
import Callout from '../../components/blog/Callout.astro';
|
||||
|
||||
Email warmup sends low-volume, human-looking mail from a mailbox and slowly raises the count so providers build a reputation for the sending domain and IP before real campaigns start. It takes 3 to 6 weeks for a brand-new domain, not 3 days, and no setting makes it faster without raising your spam risk. Here's what each piece does, with the numbers Warmbly uses.
|
||||
|
||||
## What warmup is doing, in one paragraph
|
||||
|
||||
Gmail and Outlook decide inbox versus spam partly on history: how much this domain sends, how steadily, and how recipients react. A cold domain has no history, so a sudden burst of 50 outreach emails on day one reads as a spammer who bought a domain yesterday. Warmup builds the missing history. It sends small batches of plain messages between real mailboxes, gets some opened and replied to, and ramps the daily count on a fixed curve so the pattern looks like a person's inbox slowly getting busier. It can't fix bad SPF, DKIM, or DMARC, and it can't rescue a spammy pitch. It builds the sending reputation that lets a clean pitch reach the inbox.
|
||||
|
||||
## The ramp curve: 10 a day, +1 a day, 40 ceiling
|
||||
|
||||
Warmbly starts a new mailbox at 10 warmup emails a day, adds 1 a day, and stops at 40. That's about a month of ramp: 10 on day one, 20 around day ten, 40 near day thirty. The steady +1 matters as much as the numbers. Providers watch for consistency, so a mailbox that sends 10, then 60, then 5 looks less trustworthy than one that climbs by one each day.
|
||||
|
||||
We cap at 40, not 100, on purpose. The ceiling is a warmup ceiling, not your send limit. Warmup traffic is filler. Its job is to show that the mailbox handles a modest, steady flow, and past about 40 a day the extra volume adds reputation risk without adding much signal. Your cold campaign budget sits on top of this and is capped separately: Warmbly's default cold cap is 50 emails per mailbox per day (hard max 100), with a minimum 600 seconds (10 minutes) between sends from one mailbox so the pattern isn't bursty. Warmup and campaign budgets are tracked apart, which is why raising one doesn't touch the other. More on splitting volume across inboxes in [mailbox budgets, not server settings](/blog/mailbox-budgets-not-server-settings/).
|
||||
|
||||
| Setting | Value | Why |
|
||||
| --- | --- | --- |
|
||||
| Warmup start | 10 a day | low enough that a cold domain doesn't spike |
|
||||
| Ramp | +1 a day | steady climb reads as natural growth |
|
||||
| Warmup ceiling | 40 a day | past this, risk rises faster than signal |
|
||||
| Cold cap (default) | 50 a day | separate budget, on top of warmup |
|
||||
| Min gap between sends | 600 seconds | avoids bursty patterns from one mailbox |
|
||||
|
||||
## How a warmup pool picks partner mailboxes
|
||||
|
||||
A warmup pool is a set of participating mailboxes that send to and reply to each other. When your mailbox sends a warmup message, the pool picks a partner mailbox from the same pool to receive it, and that partner may reply so providers see a two-sided conversation instead of one-way blasting.
|
||||
|
||||
The selection avoids repeat pairings. Warmbly excludes any mailbox you've been paired with in the last 7 days, so you're not sending the same two addresses back and forth all week. Repetitive pairing is a pattern spam filters can learn, and it's the opposite of what a real inbox looks like. Pool quality matters more than pool size here: a small pool of warmed, monitored mailboxes beats a large one padded with throwaway inboxes. Warmbly keeps free and premium pools separate, and a free-pool mailbox never silently joins the premium pool, because mixing low-trust senders into a paid pool spreads their risk to everyone in it. Pool mechanics in more depth are in the [warmup-pools reference](https://docs.warmbly.com/learn/warmup-pools/).
|
||||
|
||||
## How long warmup takes a new domain
|
||||
|
||||
Plan on 3 to 6 weeks for a brand-new domain to reach a stable warmed state, and don't rush it. The ramp itself is about 30 days to climb from 10 to 40 a day at +1. On top of that, reputation is cumulative: providers need repeated days of consistent, well-received mail before they trust the domain, and that trust builds over weeks, not sends.
|
||||
|
||||
Named sources land in the same range. Postmark describes domain warmup as slowly increasing volume over weeks, often stabilizing in 3 to 6 weeks. Mailgun frames IP warmup as gradually raising volume so mailbox providers can observe behavior and build reputation. The theme is consistent: gradual, observed, and measured in weeks. A domain that skips warmup and starts at 50 cold emails on day one may see early sends land, then watch placement fall once providers have enough data to score it as a bulk sender with no track record.
|
||||
|
||||
## Verification tokens: telling real warmup from gaming
|
||||
|
||||
A pool only works if the messages inside it are genuine warmup traffic. If a mailbox could claim it sent warmup mail without delivering it, or an outsider could forge pool messages, the reputation signal would be worthless. So every Warmbly warmup message carries a signed HMAC verification token, embedded as a footer reference line.
|
||||
|
||||
When the receiving side classifies a warmup message, it validates that token on five checks: the HMAC signature, a 48-hour expiry, a single-use ULID so a token can't be replayed, pool membership, and a sender/recipient match. A message that fails these isn't counted as valid warmup. Failed tokens are also an abuse signal. A mailbox that logs 3 or more invalid tokens in 24 hours gets quarantined, because forged or malformed tokens usually mean someone's trying to inflate warmup numbers without doing the sending.
|
||||
|
||||
<Callout title="Tokens catch gaming, not deliverability problems">
|
||||
A clean token record means the warmup happened as claimed. It says nothing about whether those
|
||||
messages hit the inbox. Token checks and placement checks are separate signals, and a mailbox can
|
||||
pass every token check while still landing in spam. Watch both.
|
||||
</Callout>
|
||||
|
||||
## Why a bad mailbox poisons a shared pool
|
||||
|
||||
In a shared pool, every mailbox's reputation touches every partner it sends to. A mailbox that lands in spam drags its partners into fishing messages out of the junk folder, or leaves replies unanswered, and each healthy mailbox paired with it absorbs a little damage. Left in the pool, one bad sender degrades the signal for everyone paying for a clean one.
|
||||
|
||||
Warmbly pulls mailboxes on health bands, set well below where providers start penalizing. The full band math and the reasoning for acting early is in [quarantine early](/blog/quarantine-early/).
|
||||
|
||||
| Band | Trigger | What happens |
|
||||
| --- | --- | --- |
|
||||
| Watch | 10% spam placement | lower volume, more spacing |
|
||||
| Quarantine | 20% placement, or 0.10% complaints, or 5% bounces | out of the shared pool, 7 days |
|
||||
| Block | 40% placement, or 0.30% complaints, or 10% bounces | out 30 days, manual review |
|
||||
|
||||
Those complaint numbers track named provider thresholds. Google's bulk-sender guidance says keep user-reported spam under 0.10% and never reach 0.30%. Amazon SES says keep complaint rate under 0.1% (review around 0.1%, possible pause around 0.5%) and bounce rate under 5% (review at 5%, possible pause at 10%). Warmbly's quarantine and block triggers sit at or below those, so a mailbox leaves the shared pool before provider enforcement hits your IP.
|
||||
|
||||
Time served doesn't restore a mailbox. Re-entry requires authentication intact, no recent complaint or bounce spikes, clean token behavior, spam placement back under 10% on a fresh probation sample, and re-warming from low volume (5 to 10 a day). Getting a specific mailbox back is covered in [getting a mailbox back into the warmup pool](/blog/getting-a-mailbox-back-into-warmup-pool/).
|
||||
|
||||
## Keep warming after campaigns start
|
||||
|
||||
The common mistake is treating warmup as a one-time setup you switch off once campaigns begin. Reputation decays when a domain's only traffic becomes cold outreach, so warmup should keep running alongside campaigns to hold the baseline of normal, well-received mail. The warmup budget stays separate from the cold cap, so ongoing warmup doesn't eat into your outreach volume. It runs underneath it.
|
||||
|
||||
Warmup builds reputation. It can't hold it up under a bad list or a spammy pitch, and it can't undo failing DMARC alignment (SPF and DKIM can both pass while DMARC alignment fails against your visible From domain). Pair warmup with clean authentication and a low complaint rate, and read [delivery rate is not inbox placement](/blog/delivery-rate-is-not-inbox-placement/) before you trust a delivery percentage. Warmbly is open source under Apache 2.0 at [github.com/warmbly/warmbly](https://github.com/warmbly/warmbly), and the [warmup](/warmup/) and [deliverability](/deliverability/) pages cover how the pool and tracking fit together.
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: 'How many cold emails can you actually send per day?'
|
||||
description: 'The safe number is per mailbox, not per account: 50/day default, 100 hard max, a 600s gap between sends. Here is the math to plan any daily volume.'
|
||||
pubDate: 2026-07-02
|
||||
cover: '/blog/how-many-cold-emails-per-day-per-mailbox.webp'
|
||||
tags: ['sending', 'deliverability']
|
||||
---
|
||||
import Callout from '../../components/blog/Callout.astro';
|
||||
|
||||
Fifty cold emails per mailbox per day. That's the default cap Warmbly ships, and it's a deliberately conservative number, not a technical ceiling. You can go higher (the hard max is 100 per mailbox), but 50 is the default because of what Gmail and Outlook tolerate from a sender they don't yet trust, not because of anything in the tool.
|
||||
|
||||
This post covers why that number holds, why spacing matters as much as the daily count, and how to do the mailbox math when you need 200, 500, or 1,000 sends a day.
|
||||
|
||||
## The short answer: 50 per mailbox per day
|
||||
|
||||
Warmbly caps cold campaigns at 50 emails per mailbox per day by default and enforces a hard maximum of 100. Fifty is the safe band for most cold mailboxes. One hundred is the point past which you should have positive reputation signals and a reason, not a default.
|
||||
|
||||
Why 50 and not 51? The daily count is a proxy for a pattern. A mailbox sending 50 well-spaced messages to real people looks like a person working an inbox. The same mailbox sending 300 looks like software, and the receiving provider treats it that way. It's the volume at which a normal, recently connected mailbox tends to stay out of trouble while you watch complaint and bounce signals.
|
||||
|
||||
Your sending limit isn't one server setting you turn up. It's the sum of per-mailbox budgets. We covered that split in [mailbox budgets, not server settings](/blog/mailbox-budgets-not-server-settings/), and it's the model that makes the rest of this math work.
|
||||
|
||||
## Why the limit is Gmail's and Outlook's, not your tool's
|
||||
|
||||
No cold email tool can raise the number of emails a mailbox lands in the inbox. That decision belongs to the receiving provider. Warmbly can pace your sends, sign your DKIM, and pull risky mailboxes out of the warmup pool, but the inbox-or-spam call is made by Gmail, Outlook, and Apple based on how your mail behaves over time.
|
||||
|
||||
Google's bulk-sender guidance tells you what it watches: keep the user-reported spam rate under 0.10%, never reach 0.30%, and authenticate with SPF, DKIM, and DMARC with alignment plus one-click unsubscribe for bulk mail. Amazon SES, which many senders route through, says keep your complaint rate under 0.1% (SES starts reviewing accounts around 0.1% and may pause sending near 0.5%) and your bounce rate under 5% (review at 5%, possible pause at 10%).
|
||||
|
||||
Those are the enforcers. The 50/day default sits well below the volume where a healthy mailbox would trip them, which is the point. You want headroom, not a number that dares the provider to notice you. More on reading those rules in [reading the bulk sender rules](/blog/reading-the-bulk-sender-rules/).
|
||||
|
||||
## The 600-second gap: spacing matters as much as the daily cap
|
||||
|
||||
Warmbly enforces a minimum of 600 seconds (10 minutes) between sends from a single mailbox. This matters as much as the daily number, and most advice skips it.
|
||||
|
||||
Fifty emails a day is about one every 10 minutes across an 8-hour window. That's the shape of a person sending mail, not a script firing a batch at 9:00 a.m. Two mailboxes can both send 50 a day and look completely different to Gmail: one drips them out, the other dumps them in a burst. The burst pattern is the one that gets filtered, even at the same daily count.
|
||||
|
||||
So when you plan volume, plan the clock too. A 600-second gap means one mailbox tops out around 144 sends in a 24-hour day at the theoretical limit (86,400 seconds divided by 600), but you're capping at 50 for reputation reasons, which leaves plenty of natural spacing. Don't collapse that spacing to hit a daily number faster. Bursty sending from one mailbox is a reputation risk on its own.
|
||||
|
||||
## Do the math: mailboxes for 200, 500, or 1,000 sends a day
|
||||
|
||||
Total daily volume is mailbox count times per-mailbox cap. At the 50/day default, the math is direct.
|
||||
|
||||
| Target sends/day | Mailboxes at 50/day | Mailboxes at 30/day (conservative) |
|
||||
| --- | --- | --- |
|
||||
| 200 | 4 | 7 |
|
||||
| 500 | 10 | 17 |
|
||||
| 1,000 | 20 | 34 |
|
||||
|
||||
A fresh mailbox shouldn't start at 50. Warmbly's operational posture is to open cold outreach around 10 to 20 a day, ramp slowly while the mailbox proves stable, then settle into the 30 to 50 band. So the right column (30/day) is closer to what a newer setup should plan for, and it means more mailboxes for the same volume.
|
||||
|
||||
There's a matching constraint on the infrastructure side. Warmbly runs one worker per machine, each with its own IP, and a worker's planned volume equals the sum of its mailboxes' caps. No single worker or IP should carry a large share of your total traffic. Spreading 1,000 sends across 20 mailboxes on distributed IPs is safer than concentrating them, even when the per-mailbox number is identical. We go deeper on mailbox and domain planning in [how many mailboxes and domains you need](/blog/how-many-mailboxes-and-domains-cold-email/).
|
||||
|
||||
## When it's safe to push a mailbox past 50 toward 100
|
||||
|
||||
The 100 hard max exists for mailboxes that have earned it. Moving from 50 toward 100 should follow positive signals, not a deadline.
|
||||
|
||||
Before you raise a mailbox above 50, you want:
|
||||
|
||||
- A complaint rate holding well under Google's 0.10% line, ideally near zero.
|
||||
- A bounce rate under Amazon SES's 5% review threshold, and ideally under 2%.
|
||||
- Weeks of stable sending, not days. A brand-new mailbox typically takes 3 to 6 weeks of warmup to reach a stable state (warmup itself starts at 10/day, ramps +1/day, and tops out at 40/day).
|
||||
- Clean warmup behavior with no quarantine events.
|
||||
|
||||
<Callout title="Raise volume by adding mailboxes first">
|
||||
The safest way to grow sending is more healthy mailboxes, not more emails per mailbox. Pushing a single inbox toward 100/day concentrates risk on one reputation surface. Adding mailboxes spreads that risk across IPs and domains, which is what the receiving providers reward. Treat 100 as a reachable ceiling for proven mailboxes, not a target for all of them.
|
||||
</Callout>
|
||||
|
||||
If you push a mailbox up and complaint or bounce signals move, back off. Warmbly's health bands trigger before providers do: a mailbox enters Watch at 10% spam-folder placement, then Quarantine at 20% placement or 0.10% complaints or 5% bounce, and gets pulled from the shared warmup pool. Acting at 10% placement is the point, not waiting for 40%. [Quarantine early](/blog/quarantine-early/) covers why the early action beats the late one.
|
||||
|
||||
## What the SERP gets wrong about 20 to 100 per day
|
||||
|
||||
Search "how many cold emails per day" and you'll get numbers from 20 to 100, often on the same page, with no explanation of why they differ. The advice contradicts itself because it collapses three questions into one.
|
||||
|
||||
Here's the split that resolves it:
|
||||
|
||||
| The number | What it describes |
|
||||
| --- | --- |
|
||||
| 10 to 20/day | A fresh or recently connected mailbox, still warming |
|
||||
| 30 to 50/day | A stable, warmed mailbox in the normal safe band |
|
||||
| Up to 100/day | A proven mailbox with clean reputation signals, deliberately raised |
|
||||
|
||||
All three numbers are correct. They describe mailboxes at different stages, not one universal limit. "20 per day" is right for a two-week-old mailbox and wrong for one that's been sending clean mail for two months. "100 per day" is fine for the proven mailbox and reckless for the new one. The SERP rarely says which stage it means.
|
||||
|
||||
The honest answer is a range tied to mailbox health, not a single figure. Warmbly encodes that as defaults you can see and change: 50 default, 100 max, 600-second gap, all open source under Apache 2.0 at [github.com/warmbly/warmbly](https://github.com/warmbly/warmbly). The docs at [docs.warmbly.com/learn](https://docs.warmbly.com/learn/) cover the cold-email-rules and inbox-placement detail behind each number.
|
||||
|
||||
Start each mailbox at 10 to 20, ramp toward 50 as it proves stable, and only reach for 100 with reputation to back it. Then scale total volume by adding mailboxes, not by leaning harder on the ones you have. If you want the pacing, warmup, and quarantine handled for you, that's what the [sending](/sending/) and [deliverability](/deliverability/) sides of Warmbly do.
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: 'How many mailboxes and domains do you need for the volume you want?'
|
||||
description: 'Planning cold email volume comes down to per-mailbox budgets. Work backward from your daily target to the mailboxes, domains, and IP spread you actually need.'
|
||||
pubDate: 2026-06-22
|
||||
cover: '/blog/how-many-mailboxes-and-domains-cold-email.webp'
|
||||
tags: ['sending', 'deliverability']
|
||||
---
|
||||
import Callout from '../../components/blog/Callout.astro';
|
||||
|
||||
Short version: to send N cold emails a day, divide N by a safe per-mailbox cap of 50, then add a buffer for mailboxes that are warming or resting. That puts 1,000 sends a day at around 20 to 25 active mailboxes, spread across 4 to 8 domains and never concentrated behind one IP. The rest is the math and where each number comes from.
|
||||
|
||||
## Start from the mailbox budget, not the daily target
|
||||
|
||||
Most people pick the daily number first ("I want 1,000 sends a day") and then look for a tool that will do it. That order is backwards. The daily number you can safely send is an output, not an input. It's the sum of what each of your mailboxes can carry without looking suspicious to Gmail and Outlook.
|
||||
|
||||
A mailbox's safe volume is set by the provider, per mailbox, based on how that mailbox has behaved. You don't get to raise it by paying more. You raise total volume by adding mailboxes, each with its own small budget, and letting the sum grow. We covered why that limit isn't a server setting in [your send limit is a mailbox budget](/blog/mailbox-budgets-not-server-settings/). This post is the sizing math that follows from it.
|
||||
|
||||
Warmbly defaults to 50 campaign emails per mailbox per day, with a minimum of 600 seconds (10 minutes) between sends from the same mailbox. You can push a mailbox to 100 in settings, and validation stops you there, but 50 is the number a normal mailbox with a few months of clean history can sustain without drawing attention. Treat 50 as your planning cap, not 100.
|
||||
|
||||
## The formula: daily volume divided by the per-mailbox cap, plus buffer
|
||||
|
||||
Here's the whole calculation.
|
||||
|
||||
```
|
||||
mailboxes needed = (daily volume / safe per-mailbox cap) x buffer
|
||||
```
|
||||
|
||||
Use 50 as the safe cap. The buffer covers reality: some mailboxes are still warming (they start at 10/day and ramp +1/day toward a 40/day ceiling), some are resting, and one or two may be in a health band you'd rather not send from. A buffer of 1.2 to 1.4 (add 20 to 40 percent) keeps your target reachable on a normal day.
|
||||
|
||||
For 1,000 sends a day: 1,000 / 50 = 20 mailboxes at the cap, times a 1.25 buffer, is about 25 mailboxes provisioned. You send from the healthy ones and let the rest catch up.
|
||||
|
||||
Two things the formula quietly assumes. First, every mailbox is warmed. A brand-new mailbox typically takes 3 to 6 weeks to reach a stable warmed state, so it can't contribute its full 50 on day one. Second, you keep [warmup](/warmup/) running after campaigns start, not just before, so mailboxes stay warm while they send.
|
||||
|
||||
## Per inbox, not per domain: how many mailboxes a domain can carry
|
||||
|
||||
Search results love the phrase "inboxes per domain," and the common advice is 2 to 3 mailboxes per domain. That number isn't a hard provider rule. It's a hedge against domain-level blast radius.
|
||||
|
||||
The reasoning: reputation lives at both the mailbox and the domain level. If you put 10 sending mailboxes on one domain and that domain picks up a spam reputation, all 10 go down together. Spread the same 10 across 4 domains and a single bad domain costs you 2 or 3 mailboxes, not the whole operation. Fewer mailboxes per domain means smaller correlated failures.
|
||||
|
||||
A workable rule of thumb:
|
||||
|
||||
| Mailboxes per domain | Blast radius if the domain sours | When it fits |
|
||||
| --- | --- | --- |
|
||||
| 1 to 2 | tiny | high-value or fragile sending |
|
||||
| 3 | small | most cold outreach |
|
||||
| 5+ | large | only with proven, stable domains |
|
||||
|
||||
Whatever the count, every domain needs SPF, DKIM, and DMARC with alignment before it sends. Google's bulk-sender guidance requires all three plus one-click unsubscribe for bulk mail. DMARC needs SPF or DKIM to align with the visible From domain, so both can pass authentication while alignment still fails. If that distinction is new, [SPF, DKIM, DMARC alignment](/blog/spf-dkim-dmarc-alignment-cold-email/) walks through it.
|
||||
|
||||
## Why no single IP should carry a large share of your sends
|
||||
|
||||
Splitting mailboxes across domains handles domain reputation. It does nothing for IP reputation, and that's a separate failure mode.
|
||||
|
||||
Warmbly runs one worker per machine, each with its own IP, and email accounts are assigned to specific workers. A worker's planned volume equals the sum of its mailboxes' daily caps, never an independent higher target. We don't give workers a global throttle, because that would either duplicate the per-mailbox math or silently override it. The rule is an assignment rule instead: don't pile many active sending mailboxes onto one worker, because every worker is also one IP, and concentrating traffic through one IP recreates the exact problem the per-domain split was preventing.
|
||||
|
||||
The principle holds past our architecture. Whatever sends your mail, no single IP should carry a large share of your total volume. 500 emails a day through one IP looks different to Gmail than the same 500 spread across five sending identities, even if the mailboxes and domains are otherwise identical.
|
||||
|
||||
<Callout title="One number to hold onto">
|
||||
Your planned daily volume is the sum of your mailbox caps. It is never a higher number you set on top of them, and it should never funnel through one IP. If a tool lets you type a big daily target that isn't backed by that many warmed mailboxes on distinct sending identities, the number is fiction.
|
||||
</Callout>
|
||||
|
||||
## Worked examples: mailbox and domain counts for 200, 500, 1,000 a day
|
||||
|
||||
Using a 50/day cap, a 1.25 buffer, and 3 mailboxes per domain:
|
||||
|
||||
| Daily target | Mailboxes at cap | Provisioned (with buffer) | Domains (3/domain) |
|
||||
| --- | --- | --- | --- |
|
||||
| 200 | 4 | 5 | 2 |
|
||||
| 500 | 10 | 13 | 4 to 5 |
|
||||
| 1,000 | 20 | 25 | 8 to 9 |
|
||||
|
||||
Read the 1,000 row carefully, since [mailboxes for 1,000 emails a day](/blog/how-many-cold-emails-per-day-per-mailbox/) is where most people underprovision. 20 mailboxes gets you there on paper. 25 gets you there on a Tuesday when three are resting and two are still ramping. And 8 to 9 domains keeps any single domain's failure to roughly a tenth of your capacity.
|
||||
|
||||
If you're running this for clients rather than one brand, the counts multiply fast, and domain hygiene per client matters more. See [running cold email for agencies](/use-cases/agencies/).
|
||||
|
||||
None of these counts guarantee inbox placement. They size the fleet so that no single mailbox, domain, or IP is doing too much. Placement still depends on your content, your list quality, and your complaint rate.
|
||||
|
||||
## When to add domains versus stretching existing mailboxes
|
||||
|
||||
You have two levers when you want more volume: raise per-mailbox caps toward 100, or add mailboxes and domains. Reach for caps last.
|
||||
|
||||
Raising a mailbox from 50 toward 100 is reasonable only when three numbers have been quiet for a few weeks: complaint rate, hard bounce rate, and where that mailbox's warmup messages have been landing. If you don't know those numbers, that's your answer. And the thresholds aren't yours to negotiate. Amazon SES puts an account under review around a 0.1% complaint rate and may pause it near 0.5%, and reviews bounce rates at 5%. Google says keep reported spam under 0.10% and never reach 0.30%.
|
||||
|
||||
So the order of operations:
|
||||
|
||||
- Under target and mailboxes are healthy? Add mailboxes first, on new or existing stable domains.
|
||||
- Adding mailboxes to a domain that already carries 3? Add a domain instead.
|
||||
- Every mailbox pristine for weeks and still short? Then nudge a few caps from 50 toward 60 or 70, one step at a time, watching complaints and bounces.
|
||||
- A mailbox landing in spam over 20% on 20+ warmup deliveries in 7 days? It's quarantined and out of your budget until it requalifies. Don't backfill by overloading its neighbors. See [quarantine early](/blog/quarantine-early/).
|
||||
|
||||
Stretching existing mailboxes trades a little setup work for concentrated risk. Adding mailboxes and domains trades more setup work for spread-out risk. For cold email, spread wins almost every time.
|
||||
|
||||
Warmbly is open source under Apache 2.0 ([github.com/warmbly/warmbly](https://github.com/warmbly/warmbly)), so you can run the whole thing yourself: [sending](/sending/), warmup, and per-mailbox tracking. The docs at [docs.warmbly.com/learn](https://docs.warmbly.com/learn/) cover warmup pools and inbox placement if you want the mechanics before you size your fleet.
|
||||
@@ -2,6 +2,7 @@
|
||||
title: 'Your send limit is a mailbox budget, not a server setting'
|
||||
description: 'People keep asking us to raise their daily limit. The honest answer is that the limit they are asking about does not exist.'
|
||||
pubDate: 2026-06-10
|
||||
cover: '/blog/mailbox-budgets-not-server-settings.webp'
|
||||
tags: ['sending', 'deliverability']
|
||||
---
|
||||
|
||||
@@ -17,8 +18,8 @@ The 600-second gap matters more than people think. Mailbox providers see timing.
|
||||
|
||||
More mailboxes. That's the whole answer.
|
||||
|
||||
If you need 1,000 sends a day, that is twenty mailboxes at the default cap, ideally spread over a few domains. It is genuinely more setup work than typing 1000 into a box, and it is also the only version that survives contact with Gmail.
|
||||
If you need 1,000 sends a day, that is twenty mailboxes at the default cap, ideally spread over a few domains. We work that arithmetic out for 200, 500, and 1,000 a day in [how many mailboxes and domains you need](/blog/how-many-mailboxes-and-domains-cold-email/). It is genuinely more setup work than typing 1000 into a box, and it is also the only version that survives contact with Gmail.
|
||||
|
||||
This shapes our infrastructure too, in a way that took us a while to get right. Workers (the processes that actually deliver mail) do not have their own send limits. A worker's capacity is the sum of the budgets of the mailboxes assigned to it, nothing more. Early on we were tempted to give workers a global throttle as a safety net, and we ended up removing the idea: it either duplicates the per-mailbox math or silently overrides it, and both are bugs. What we kept instead is an assignment rule. We don't pile many active sending mailboxes onto one worker, because every worker is also an IP address, and concentrating traffic through one IP recreates the exact problem the mailbox budgets were preventing.
|
||||
This shapes our [sending infrastructure](/sending/) too, in a way that took us a while to get right. Workers (the processes that actually deliver mail) do not have their own send limits. A worker's capacity is the sum of the budgets of the mailboxes assigned to it, nothing more. Early on we were tempted to give workers a global throttle as a safety net, and we ended up removing the idea: it either duplicates the per-mailbox math or silently overrides it, and both are bugs. What we kept instead is an assignment rule. We don't pile many active sending mailboxes onto one worker, because every worker is also an IP address, and concentrating traffic through one IP recreates the exact problem the mailbox budgets were preventing.
|
||||
|
||||
One more thing about raising the cap, since the settings page will let you go to 100. Before you do, look at three numbers for that mailbox: complaint rate, hard bounce rate, and where your warmup messages have been landing. If all three have been quiet for a few weeks, going from 50 to 60 or 70 is reasonable. If you don't know what those numbers are, that is itself the answer.
|
||||
One more thing about raising the cap, since the settings page will let you go to 100. Before you do, look at three numbers for that mailbox: [complaint rate, hard bounce rate](/blog/complaint-rate-that-gets-you-throttled/), and where your warmup messages have been landing. If all three have been quiet for a few weeks, going from 50 to 60 or 70 is reasonable. If you don't know what those numbers are, that is itself the answer.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: 'An open source cold email and warmup platform: what self-hosting actually gives you'
|
||||
description: 'Warmbly is Apache 2.0 cold email plus warmup you can self-host. What running it yourself means for IPs, workers, and your data, and when cloud is the better call.'
|
||||
pubDate: 2026-07-04
|
||||
cover: '/blog/open-source-cold-email-platform.webp'
|
||||
tags: ['deliverability', 'sending', 'open-source']
|
||||
---
|
||||
import Callout from '../../components/blog/Callout.astro';
|
||||
|
||||
Self-hosting Warmbly gives you a full cold email and warmup stack you run on your own IPs, under Apache 2.0, with mailbox credentials and message content sealed with keys you control. What it can't give you is the shared warmup pool, because a pool needs many organizations' mailboxes talking to each other, and one instance is one organization.
|
||||
|
||||
That trade is the point of this post. Here's what you own, what you have to run yourself, and where the coming cloud does something a single self-hosted box can't.
|
||||
|
||||
## Why an open source cold email tool is worth wanting
|
||||
|
||||
Most cold email tools are closed SaaS: your mailbox passwords, your prospect lists, and your reply content live on someone else's servers, and you find out how sending works when it breaks. An [open source cold email platform](https://github.com/warmbly/warmbly) changes what you can inspect and control. You can read how the scheduler paces sends, how tokens get validated, and what gets encrypted before it hits disk.
|
||||
|
||||
Three concrete things you get that a closed tool won't give you:
|
||||
|
||||
- The sending logic is auditable. When a mailbox stops sending, you can read the guardrail that stopped it instead of filing a support ticket.
|
||||
- Your data stays on infrastructure you picked. No third party holds your mailbox OAuth tokens or the bodies of your replies.
|
||||
- No per-seat or per-mailbox billing on the software itself. You pay for servers and IPs, not a license.
|
||||
|
||||
If your only goal is to send today with zero ops, that's a real reason to wait for hosted. If you care about controlling where credentials live and how sending behaves, running the code yourself is the way to get both.
|
||||
|
||||
## What you run when you self-host Warmbly
|
||||
|
||||
Warmbly isn't one binary. It's six services, and self-hosting means operating all of them:
|
||||
|
||||
| Service | Language | Job |
|
||||
| --- | --- | --- |
|
||||
| Backend | Go | API and business logic, the control plane |
|
||||
| Consumer | Go | reads events, updates platform state |
|
||||
| Workers | Go | execute sends and mailbox sync, one per machine |
|
||||
| Tracking | Rust | serves open pixels and click redirects |
|
||||
| Realtime | Elixir | websocket fanout to the dashboard |
|
||||
| Dashboard | React | the in-product UI |
|
||||
|
||||
Behind those sit Postgres, Redis, and Kafka. The workers are the part that makes distribution work: each runs on its own machine with its own IP, and email accounts get assigned to specific workers. Planned volume for a worker equals the sum of its mailboxes' daily caps, so no single worker or IP becomes a concentration point for a large share of your traffic. That spread is a deliverability decision, and self-hosting means you decide how many machines to run.
|
||||
|
||||
The dashboard, tracking, and realtime services are the same code that will back the hosted product. Nothing is stubbed for the open source build.
|
||||
|
||||
## The parts self-hosting makes you own: IPs, domains, and worker spread
|
||||
|
||||
This is the honest part. Running the code is the easy 20%. The other 80% is the sending infrastructure the code assumes you have.
|
||||
|
||||
You own the IPs. Warmbly spreads sending across worker IPs to avoid concentration, but it can't manufacture reputation for an IP that's never sent mail. A fresh IP has no history, and mailbox providers treat it that way until it proves itself.
|
||||
|
||||
You own the domains and their auth. SPF has a 10 DNS-lookup limit, and going over it breaks SPF. DMARC needs SPF or DKIM to align with the visible From domain, so both can pass authentication while DMARC alignment still fails. Google's bulk-sender guidance requires SPF, DKIM, and DMARC with alignment plus one-click unsubscribe for bulk mail. Getting that right is on you. Our [SPF, DKIM, DMARC guide](https://docs.warmbly.com/learn/spf-dkim-dmarc/) and the [alignment post](/blog/spf-dkim-dmarc-alignment-cold-email/) walk through the failure modes.
|
||||
|
||||
You own deliverability monitoring. Warmbly runs its own [open and click tracking](/analytics/) and reports the signals, but you decide what to do when complaint rate climbs toward Amazon SES's 0.1% review line or Google's 0.10% reported-spam threshold. The product's defaults help: a 50 emails/mailbox/day cold cap (hard max 100), a minimum 600 seconds between sends from one mailbox, and warmup that starts at 10/day and ramps +1/day to a 40/day ceiling. A brand-new mailbox typically takes 3 to 6 weeks to reach a stable warmed state. Those numbers ship in the code. Whether your IPs and domains can sustain them is your call. See [how many mailboxes and domains you need](/blog/how-many-mailboxes-and-domains-cold-email/) and [budgets, not server settings](/blog/mailbox-budgets-not-server-settings/).
|
||||
|
||||
<Callout title="The shared warmup pool is a cloud feature, not a self-host feature" variant="warn">
|
||||
A warmup pool works by pairing your mailboxes with other participants so providers see two-way conversation. A single self-hosted instance is one organization, so its mailboxes can only talk to each other, which providers learn to recognize. The pool mechanics (partner selection from the same pool, recent partners excluded for 7 days, HMAC-signed verification tokens, health-band quarantine) are in the code, but the network effect that makes a pool credible needs many organizations. That's the [hosted cloud](/pricing/), coming soon.
|
||||
</Callout>
|
||||
|
||||
## How the encryption model keeps credentials and message content private
|
||||
|
||||
The design assumption is that mailbox passwords, OAuth tokens, and message content are secrets, and the storage layer should never see them in the clear.
|
||||
|
||||
The chain works like this. AWS KMS is the root of trust. Each organization gets its own data encryption key (a DEK), generated by KMS. Sensitive fields are sealed with AES-256-GCM using that DEK, and only the encrypted DEK blob is stored. The plaintext DEK is cached with a TTL, then dropped. Keys are per-organization, never per-user, so offboarding one person never orphans a mailbox the whole org depends on.
|
||||
|
||||
Two properties matter for self-hosting. First, workers never open a Postgres connection. When a worker needs a DEK to decrypt a mailbox credential, it fetches it over the backend's internal HTTP API, so the SQL blast radius stays on the control plane, not on the many disposable machines doing the sending. Second, because you hold the KMS key, no one outside your AWS account can decrypt a stored field. That's the concrete meaning of your credentials staying private: it's enforced by key custody, not a privacy policy. More on the model in [what we do with your data](/trust/).
|
||||
|
||||
## Self-host vs the coming cloud: an honest decision table
|
||||
|
||||
| Question | Self-host | Cloud (coming soon) |
|
||||
| --- | --- | --- |
|
||||
| Who runs the six services? | You | Warmbly |
|
||||
| Who owns the IPs and domains? | You | You (mailboxes), Warmbly (sending infra) |
|
||||
| Who holds the KMS key? | You | Warmbly |
|
||||
| Shared warmup pool? | No, single-org only | Yes, cross-org pool |
|
||||
| Deliverability monitoring? | You watch and react | Managed with the same thresholds |
|
||||
| Best fit | You want full data custody and control, and can run infra | You want to send without operating Kafka and IP pools |
|
||||
|
||||
Neither is strictly better. If you can run distributed infrastructure and you want your credentials never to leave keys you hold, self-host. If you want sending plus a real warmup pool without operating any of it, the [cloud waitlist](https://app.warmbly.com) is open, and the [14-day trial](/pricing/) gives you 3 mailboxes with no credit card. The free tier exists to try the product, not to run production, and its warmup pool is deliberately not filled.
|
||||
|
||||
## Where the code lives and what Apache 2.0 lets you do
|
||||
|
||||
The code is at [github.com/warmbly/warmbly](https://github.com/warmbly/warmbly) under Apache 2.0. That license lets you use it commercially, modify it, run it privately, and redistribute it, including as part of a product you sell. Apache 2.0 also includes an express patent grant from contributors, so you're not exposed to a contributor asserting a patent on the code they gave you. The main obligations are keeping the license and notices, and stating significant changes you make.
|
||||
|
||||
In practice, you can fork the scheduler to change how sends are paced, add a provider integration, or wire the tracking service into your own analytics, and ship the result. If you're building on the API rather than the source, start at the [developer docs](/developers/) and the [learn section](https://docs.warmbly.com/learn/), which cover warmup, inbox placement, and reputation recovery.
|
||||
|
||||
A fair starting move: clone the repo, read `internal/scheduler` to see the 50/day cap and 600-second gap enforced in code, then decide whether you'd rather run that yourself or let the cloud run it for you. Either way, you've read the sending logic before you trusted it, which is the thing a closed tool never lets you do.
|
||||
@@ -1,36 +1,70 @@
|
||||
---
|
||||
title: 'Quarantine early: how we protect shared warmup pools'
|
||||
description: 'The industry default is to pull a mailbox from warmup when it is already landing in spam 80% of the time. We think that is roughly 70 points too late.'
|
||||
description: 'Most warmup tools pull a mailbox at 80% spam placement. Warmbly quarantines at 20%, on separate signals with sample floors, and re-entry you have to earn.'
|
||||
pubDate: 2026-06-09
|
||||
cover: '/blog/quarantine-early.webp'
|
||||
tags: ['warmup', 'policy']
|
||||
---
|
||||
import Callout from '../../components/blog/Callout.astro';
|
||||
|
||||
While building our warmup pools we looked at how other tools decide when a mailbox no longer belongs in one. The pattern we kept finding: a mailbox gets removed when its spam placement crosses something like 80%.
|
||||
Most warmup tools pull a mailbox when its spam placement crosses something like 80%. Warmbly's first action fires at 10%, and a full removal from the shared pool happens at 20%. The bands sit well below where providers start penalizing, they judge each failure signal separately, and none of them fire on a single bad day. Here is why each line is where it is, and why re-entry is something a mailbox earns back rather than waits out.
|
||||
|
||||
Think about what 80% means in a shared pool. The pool works by having participating mailboxes send to and reply to each other, so providers see normal conversational traffic. A mailbox that is landing in spam four times out of five has been poisoning those interactions for weeks. Its partners have been opening junk folders to fish its messages out (or not), its replies have been going unanswered, and every healthy mailbox paired with it has absorbed a little of that damage. Removing it at 80% is not a policy. It is a memorial service.
|
||||
## What 80% actually costs a shared pool
|
||||
|
||||
So our bands are set much lower, and they went through a few revisions before we were happy:
|
||||
Think about what 80% means. A [warmup pool](/blog/how-email-warmup-actually-works/) works by having participating mailboxes send to and reply to each other, so providers see normal conversational traffic. A mailbox that is landing in spam four times out of five has been poisoning those interactions for weeks. Its partners have been opening junk folders to fish its messages out (or not), its replies have gone unanswered, and every healthy mailbox paired with it has absorbed a little of that damage. Removing it at 80% is not a policy. It is a memorial service.
|
||||
|
||||
The cost is shared, which is the whole reason to act early. In a pool, one mailbox's reputation touches every partner it sends to. The healthy paying customers are the asset worth protecting. The failing mailbox is the cost the rest of them absorb.
|
||||
|
||||
## The bands, and why each line sits where it does
|
||||
|
||||
Our bands went through a few revisions before we were happy with them:
|
||||
|
||||
| Band | Trigger | What happens |
|
||||
| --- | --- | --- |
|
||||
| Watch | 10% spam placement | volume down, spacing up |
|
||||
| Quarantine | 20% placement, or 0.10% complaints, or 5% bounces | out of the pool, 7 days |
|
||||
| Block | 40% placement, or 0.30% complaints | out 30 days, manual review |
|
||||
| Block | 40% placement, or 0.30% complaints, or 10% bounces | out 30 days, manual review |
|
||||
| Catastrophic | 80% placement | long block, full reset |
|
||||
|
||||
Two design decisions worth explaining.
|
||||
The complaint and bounce triggers are not arbitrary. They are anchored to the exact points Google and Amazon SES escalate. Quarantine's 0.10% complaints and 5% bounce are where SES opens an account review. Block's 0.30% complaints and 10% bounce are where Google moves toward rejection and SES may pause sending outright. So quarantine fires a full band before the provider cliff, while the mailbox's reputation can still recover. The [enforcement numbers, provider by provider](/blog/complaint-rate-that-gets-you-throttled/) are worth reading alongside this.
|
||||
|
||||
First, the signals stay separate. An earlier draft combined everything into one health score, and we walked away from it because a single number hides what is actually wrong. A bounce problem is a list-quality problem. A complaint problem is a content or targeting problem. A token problem (every warmup message we send carries a verification token, and we count attempts with missing or forged ones) is usually someone trying to game the pool. Same quarantine, different ways back out.
|
||||
## Separate signals, not one blended score
|
||||
|
||||
Second, none of this fires on one bad data point. Placement is judged on at least 20 warmup deliveries across a week. A single message in spam on a Tuesday does not quarantine anyone.
|
||||
An earlier draft rolled everything into a single health score, and we walked away from it, because one number hides what is actually wrong. A bounce problem is a list-quality problem. A complaint problem is a content or targeting problem. A placement problem is usually reputation or authentication. A token problem (every warmup message we send carries a verification token, and we count attempts with missing or forged ones) is usually someone trying to game the pool. Same quarantine, four different ways back out.
|
||||
|
||||
Collapse them into one score and you fix the wrong thing. Re-warming a mailbox whose real problem is a scraped list burns it again at lower volume. The signal you separate out is the repair you make.
|
||||
|
||||
## Good engagement does not cancel a complaint
|
||||
|
||||
Open and reply rates are a weak positive signal, and we treat them that way. A mailbox with healthy engagement and a rising complaint rate still gets quarantined, because the people hitting "report spam" are not the people opening the mail, and one group's approval does not buy back the other group's damage. Engagement can nudge a borderline mailbox. It cannot offset placement or complaints. That is also why open rate is the wrong number to judge a mailbox on: [Apple and Gmail inflate opens](/blog/getting-a-mailbox-back-into-warmup-pool/) by prefetching images whether or not a human looked, so a burned mailbox can post a healthy open rate the whole time it is landing in spam.
|
||||
|
||||
## Nothing fires on one bad day
|
||||
|
||||
A band only triggers on enough data to mean something. Placement is judged on at least 20 warmup deliveries across 7 days, so a single message in spam on a Tuesday quarantines nobody. Complaints get their own floor, at least 100 delivered messages over 30 days, before the rate is worth acting on, because one complaint out of ten sends is not a 10% complaint rate, it is noise.
|
||||
|
||||
The sample floors matter as much as the thresholds. A strict threshold read off a tiny sample is a random number generator with a serious face. Wait for the floor, then act on the number.
|
||||
|
||||
## Token forgery is its own quarantine
|
||||
|
||||
Placement and complaints are reputation signals. Token behavior is an abuse signal, and it gets its own trigger. Every warmup message carries a signed verification token, and a mailbox that logs 3 or more invalid tokens in 24 hours gets quarantined regardless of its placement, because forged or malformed tokens usually mean someone is inflating warmup numbers without doing the sending. The [token mechanics](/blog/how-email-warmup-actually-works/) are in the warmup walkthrough.
|
||||
|
||||
<Callout title="Where the numbers come from">
|
||||
Google asks senders to keep reported spam under 0.10% and to never reach 0.30%. Our
|
||||
quarantine and block triggers sit at exactly those two numbers, on purpose: by the time a
|
||||
mailbox is there, the question is not whether to act but whether we already acted too late.
|
||||
Google asks senders to keep reported spam under 0.10% and to never reach 0.30%. Amazon SES puts
|
||||
accounts under review around a 0.1% complaint rate and a 5% bounce rate. Our quarantine and block
|
||||
triggers sit at exactly those numbers, on purpose: by the time a mailbox is there, the question is
|
||||
not whether to act but whether we already waited too long.
|
||||
</Callout>
|
||||
|
||||
The part people push back on is re-entry. The 7 days expiring does not put a mailbox back in the pool. It has to requalify: authentication intact, no new complaint or bounce spikes, clean token behaviour, and placement back under 10% on a fresh probation sample, starting from low volume again. We had a few users annoyed by this, usually with one specific mailbox they were sure was fine. Checking the data, it mostly wasn't.
|
||||
## Re-entry is not a timer
|
||||
|
||||
If you only take one number from this post, take 20%, not 80%. That is the point where a mailbox stops being a participant in a shared pool and starts being a cost to everyone else in it.
|
||||
The 7 days expiring does not put a mailbox back in the pool. It has to requalify: authentication intact, no new complaint or bounce spikes, clean token behavior, and placement back under 10% on a fresh probation sample, starting from low volume again. Time served proves nothing. Two weeks of sending bad mail recovers nothing; two weeks of clean, spaced, low-volume sending is what moves the number. The full sequence is in our [reputation recovery playbook](/blog/getting-a-mailbox-back-into-warmup-pool/).
|
||||
|
||||
The cleanest version of this keeps a recovering mailbox off the shared pool entirely while it climbs back, on isolated infrastructure rather than the surface healthy customers depend on. A mailbox digging out of a hole should not do it inside the pool it was damaging.
|
||||
|
||||
We have had users annoyed by the requalification step, usually over one specific mailbox they were sure was fine. Checking the data, it mostly was not. That is the pattern that convinced us the bands should be strict: the mailbox owner is the last person positioned to see placement, because they cannot see the spam folder their own mail lands in.
|
||||
|
||||
## The one number to keep
|
||||
|
||||
If you take one number from this post, take 20%, not 80%. That is the point where a mailbox stops being a participant in a shared pool and starts being a cost to everyone else in it. Every band above it is cleanup, and the point of quarantining early is to never need those bands at all.
|
||||
|
||||
Warmbly is open source under Apache 2.0 ([github.com/warmbly/warmbly](https://github.com/warmbly/warmbly)), and the bands here are the ones the pool actually enforces. The [warmup](/warmup/) and [deliverability](/deliverability/) pages cover how the pool and health tracking fit together.
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
title: "What Google's bulk sender rules actually ask of cold emailers"
|
||||
description: 'We re-read the requirements so you do not have to. Most of it is things you should have been doing anyway, but two details catch people off guard.'
|
||||
pubDate: 2026-06-08
|
||||
tags: ['deliverability']
|
||||
---
|
||||
|
||||
Every few months the bulk sender requirements make the rounds again and someone asks us whether cold email is "dead now". We have read these documents more times than we would like. Here is what they actually say, and the two places where senders genuinely get caught out.
|
||||
|
||||
The headline requirements: SPF and DKIM on your sending domain, a DMARC policy published, alignment between the From domain and what got authenticated, one-click unsubscribe on bulk mail, and a user-reported spam rate that stays under 0.10% and never touches 0.30%.
|
||||
|
||||
Most of that list is hygiene that predates the rules. The two that bite:
|
||||
|
||||
**Alignment, not just authentication.** This is the one we see most often in support. A domain can pass SPF and DKIM and still fail DMARC, because the passing signature belongs to a sending service's domain instead of yours. Dashboards love showing green checkmarks for "SPF: pass, DKIM: pass" while alignment is quietly broken. The only check we trust is opening the headers of a real delivered message and reading the `Authentication-Results` line yourself. If `dkim=pass` is followed by a `header.d=` that is not your domain, you have work to do.
|
||||
|
||||
**The spam rate math at small volume.** 0.10% sounds like a generous allowance until you do the division. A mailbox sending 50 cold emails a day sends about 1,500 a month. Staying under 0.10% means at most one spam complaint per month per mailbox. One. There is no clever trick that gets you under that threshold; the message has to be relevant enough that essentially nobody who receives it is angry about receiving it. Every other deliverability technique is downstream of that.
|
||||
|
||||
On unsubscribe: yes, the one-click requirement (RFC 8058, the List-Unsubscribe headers) is written for marketing mail to subscribers, and you can argue about whether a two-line intro email is "bulk". We would skip the argument. The recipient who wants out has exactly two buttons available, and one of them reports you to Google. Giving them the other one is not compliance, it is self-interest. Warmbly adds the headers and treats any opt-out as workspace-wide suppression, not a per-campaign flag.
|
||||
|
||||
Worth reading alongside this: Microsoft's guidance for Exchange Online, which says more or less openly that bulk commercial mail does not belong on their platform and should go through specialized providers. People read that as hostility. We read it as a description of the line both providers are drawing: traffic that looks like a person corresponding survives, traffic that looks like infrastructure gets pushed out. The bulk sender rules are that same line, written down with numbers attached.
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: "What Google's bulk sender rules actually ask of cold emailers"
|
||||
description: 'SPF, DKIM, DMARC alignment, one-click unsubscribe, spam under 0.10%: Google and Microsoft now enforce the same bulk bar. The checklist, and the two gotchas.'
|
||||
pubDate: 2026-06-08
|
||||
cover: '/blog/reading-the-bulk-sender-rules.webp'
|
||||
tags: ['deliverability']
|
||||
---
|
||||
import Callout from '../../components/blog/Callout.astro';
|
||||
|
||||
Google's bulk sender rules reduce to a short list: SPF and DKIM, a DMARC policy that aligns with your visible From domain, one-click unsubscribe on bulk mail, and a user-reported spam rate under 0.10%. Most of it is hygiene you should already run. Two details catch cold senders out, and in 2026 the cost of missing them went up: Gmail stopped quietly spam-foldering non-compliant mail and started rejecting it outright. Microsoft now enforces the same list.
|
||||
|
||||
## The full checklist, in one place
|
||||
|
||||
Google defines a bulk sender as anyone sending 5,000 or more messages to Gmail in a 24-hour window. Microsoft draws the same 5,000-a-day line for Outlook.com, Hotmail, and Live. Cross it and you land in the strict tier. Almost everything on the list is what any sender should do at any volume, but past 5,000 a day it stops being optional.
|
||||
|
||||
| Requirement | What it means |
|
||||
| --- | --- |
|
||||
| SPF | a DNS record listing the servers allowed to send for your domain |
|
||||
| DKIM | a cryptographic signature on every message, tied to your domain |
|
||||
| DMARC | a published policy (`p=none` is the floor) that requires SPF or DKIM to align |
|
||||
| Alignment | the authenticated domain matches the `From:` address the recipient reads |
|
||||
| One-click unsubscribe | a `List-Unsubscribe` header (RFC 8058) plus a visible link in the body |
|
||||
| Spam rate | user-reported spam under 0.10% in Postmaster Tools, never reaching 0.30% |
|
||||
| Valid PTR / rDNS | reverse DNS on your sending IPs that resolves back to the sending host |
|
||||
| TLS | messages transmitted over TLS, not in the clear |
|
||||
| Honest headers | RFC 5322 formatting, a real `From:`, no impersonating Gmail or another domain |
|
||||
|
||||
The quiet ones are PTR and TLS. Nobody talks about them because most managed sending already sets them, but a self-run mailserver on a fresh IP can ship without reverse DNS and fail the check before a single recipient sees the mail. For the DNS records themselves, our [SPF, DKIM, and DMARC guide](https://docs.warmbly.com/learn/spf-dkim-dmarc/) has the setup. The rest of this post is about the parts that pass a checker and still fail in production.
|
||||
|
||||
## Microsoft joined Google, so there is one bar now
|
||||
|
||||
For a while the read on Microsoft was that Exchange Online discouraged bulk commercial mail and pointed senders at specialized providers. That stopped being the interesting part. Since May 5, 2025, Outlook.com, Hotmail, and Live require SPF, DKIM, and DMARC with alignment for any domain sending more than 5,000 messages a day. Microsoft junk-foldered non-compliant mail first, then moved to rejection with a `550 5.7.515` error, the same escalation Google ran a year earlier.
|
||||
|
||||
So the two providers that hold most consumer inboxes now enforce the same authentication bar. There is no configuration that satisfies one and not the other. Get alignment right once and it holds for both. Miss it and both bounce you. Both are drawing the same line, and it is worth saying plainly: traffic that looks like a person corresponding gets through, traffic that looks like infrastructure gets pushed out. The bulk sender rules are that line written down with numbers attached.
|
||||
|
||||
## The first gotcha: alignment is not authentication
|
||||
|
||||
This is the one that shows up most in support. A domain passes SPF and passes DKIM and still fails DMARC, because the signature that passed belongs to a sending tool's domain instead of yours. Dashboards love showing green rows for "SPF: pass, DKIM: pass" while alignment is quietly broken underneath.
|
||||
|
||||
Authentication asks whether a signature verified. Alignment asks whether the verified domain matches the `From:` a human reads. You can clear the first and fail the second, and DMARC passes only if at least one of SPF or DKIM aligns with your visible `From:` domain. The only check worth trusting is opening a delivered message and reading its `Authentication-Results` header yourself. If `dkim=pass` is followed by a `header.d=` that is not your domain, you have work to do. We read a real header tag by tag in [why your green checkmarks can still lie](/blog/spf-dkim-dmarc-alignment-cold-email/).
|
||||
|
||||
## The second gotcha: 0.10% is smaller than it sounds
|
||||
|
||||
0.10% reads as a generous allowance until you divide it by cold-email volume. A mailbox sending 50 cold emails a day sends about 1,500 a month. Staying under 0.10% means at most one spam complaint per mailbox per month. One. There is no filtering trick downstream of that number: the message has to be relevant enough that essentially nobody who receives it is angry about receiving it.
|
||||
|
||||
That reframes what every other tactic is for. Warmup, authentication, and spacing all sit downstream of writing mail the recipient does not resent. The full enforcement math, Google and Amazon SES side by side, is in [what complaint, bounce, and spam rate gets you throttled](/blog/complaint-rate-that-gets-you-throttled/).
|
||||
|
||||
<Callout title="Enforcement got teeth in 2026" variant="warn">
|
||||
Through 2024 and 2025, a blown spam rate mostly meant spam-foldering, a soft signal you could
|
||||
recover from. By 2026 the enforcement had escalated to rejection: non-compliant bulk mail gets a
|
||||
550 error and never reaches any folder, not even junk. The 0.10% target did not change. What
|
||||
changed is that crossing it now costs you delivery, not just placement.
|
||||
</Callout>
|
||||
|
||||
## What "cold" changes about the rules
|
||||
|
||||
The rules were written for marketing mail sent to people who subscribed. You can argue a two-line intro email is not "bulk" in that sense, and whether one-click unsubscribe (RFC 8058, the `List-Unsubscribe` headers) even applies to it. Skip the argument. The recipient who wants out has two buttons in front of them, and one of them reports you to Google. Handing them the other one is not compliance, it is self-interest. Warmbly adds the headers and treats any opt-out as workspace-wide suppression rather than a per-campaign flag, so a person who leaves one sequence does not get hit by the next.
|
||||
|
||||
The other cold-specific reality is that you reach these thresholds at low absolute volume. A marketing team sending 5,000 a day has statistical room to absorb a handful of complaints. A cold mailbox at 50 a day has room for almost none. The rules bite harder on you precisely because your sample is smaller, which is why [spreading volume across more mailboxes and domains](/blog/how-many-mailboxes-and-domains-cold-email/) is a compliance move, not only a scaling one.
|
||||
|
||||
## The short version
|
||||
|
||||
Run SPF, DKIM, and DMARC, and confirm alignment by reading a real header instead of a dashboard. Add one-click unsubscribe and suppress opt-outs everywhere. Keep reported spam under 0.10%, which at cold volume means roughly one complaint per mailbox per month. Do that and you clear both Google's and Microsoft's bar, because in 2026 it is the same bar.
|
||||
|
||||
Warmbly is open source under Apache 2.0, so you can read exactly how the sending path handles authentication, unsubscribe headers, and suppression at [github.com/warmbly/warmbly](https://github.com/warmbly/warmbly). The [deliverability](/deliverability/) page collects the thresholds in one place.
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: 'SPF, DKIM, DMARC alignment: why your green checkmarks can still lie'
|
||||
description: 'SPF and DKIM can both pass while DMARC alignment quietly fails. How to read your Authentication-Results header and confirm the From domain actually aligns.'
|
||||
pubDate: 2026-06-24
|
||||
cover: '/blog/spf-dkim-dmarc-alignment-cold-email.webp'
|
||||
tags: ['deliverability']
|
||||
---
|
||||
import Callout from '../../components/blog/Callout.astro';
|
||||
|
||||
Your mail can pass SPF, pass DKIM, and still fail DMARC. Authentication passing and alignment passing are two different checks, and the second one is where cold email quietly breaks while your vendor dashboard shows green.
|
||||
|
||||
This post walks through the mechanism, reads a real `Authentication-Results` header line by line, and gives you a 5-minute audit you can run today. If you want the full setup reference, the [SPF, DKIM, and DMARC guide](https://docs.warmbly.com/learn/spf-dkim-dmarc/) covers the DNS records themselves.
|
||||
|
||||
## Authentication passing is not the same as alignment passing
|
||||
|
||||
DMARC does two things when a message arrives. It checks that SPF or DKIM passed, and then it checks that the passing one lines up with the domain in your visible `From:` address. That second step is alignment. You can clear the first and fail the second. When you do, DMARC fails even though both `spf=pass` and `dkim=pass` are sitting right there in the header.
|
||||
|
||||
That's the trap. A sending service signs your mail with its own domain, the signature verifies cleanly, and DMARC still returns `dmarc=fail` because the domain that authenticated isn't yours. Google's bulk sender guidance requires SPF, DKIM, and DMARC with alignment for anyone sending bulk or marketing mail, so this isn't a technicality you can skip.
|
||||
|
||||
## SPF, DKIM, and DMARC in one sentence each
|
||||
|
||||
SPF is a DNS record listing which servers can send mail for your domain, checked against the envelope sender (the `MAIL FROM`, not the `From:` you see).
|
||||
|
||||
DKIM attaches a cryptographic signature to each message, tied to a domain in the signature's `d=` tag, so the receiver can confirm the mail wasn't forged or tampered with.
|
||||
|
||||
DMARC ties the two together. It tells receivers what to do when a message fails both, and it requires that at least one of SPF or DKIM aligns with the `From:` domain the recipient actually reads.
|
||||
|
||||
## Why one of SPF or DKIM must align with your From domain
|
||||
|
||||
Alignment exists because the `From:` address is the only sender identity a human sees, and it's trivial to forge on its own. SPF checks a hidden envelope address. DKIM checks whatever domain is in the signature. Neither is guaranteed to match the friendly `From:` unless DMARC forces the question.
|
||||
|
||||
So DMARC passes only if one of these holds:
|
||||
|
||||
- **SPF alignment:** the `MAIL FROM` domain matches your `From:` domain
|
||||
- **DKIM alignment:** the DKIM `d=` domain matches your `From:` domain
|
||||
|
||||
You need one, not both. Most senders should run relaxed alignment, which matches on the organizational domain, so `bounce.outreach.acme.com` aligns with `outreach@acme.com`. Strict alignment demands an exact match and only matters when you're blocking a specific subdomain spoof. The common cold-email failure: a third-party sender authenticates everything under its own domain, so nothing aligns with yours, and DMARC fails despite two green passes.
|
||||
|
||||
## Reading a real Authentication-Results header line by line
|
||||
|
||||
Open a message you sent to a Gmail or Outlook account, show the original or raw source, and find the `Authentication-Results` line. Here's one that passes authentication but fails alignment:
|
||||
|
||||
```
|
||||
Authentication-Results: mx.google.com;
|
||||
spf=pass (google.com: domain of bounce@mail.sendingtool.com
|
||||
designates 198.51.100.7 as permitted sender)
|
||||
smtp.mailfrom=bounce@mail.sendingtool.com;
|
||||
dkim=pass header.i=@mail.sendingtool.com header.s=s1 header.b=Ab3xQ;
|
||||
dmarc=fail (p=NONE sp=NONE dis=NONE) header.from=acme.com
|
||||
```
|
||||
|
||||
Read it in order. `spf=pass`, good, but `smtp.mailfrom=bounce@mail.sendingtool.com` is the vendor's domain, not `acme.com`. Then `dkim=pass`, good, but `header.i=@mail.sendingtool.com` is again the vendor. The `From:` the recipient sees is `header.from=acme.com`. Neither authenticated domain matches it, so `dmarc=fail`.
|
||||
|
||||
The two tags that decide alignment are `smtp.mailfrom` (for SPF) and `header.i` or `header.d` (for DKIM). If neither matches your `From:` domain, you have work to do. A healthy result shows `dkim=pass header.d=acme.com` and `dmarc=pass` with `header.from=acme.com`.
|
||||
|
||||
## The green-checkmark trap in vendor dashboards
|
||||
|
||||
Dashboards tend to render three rows: SPF pass, DKIM pass, DMARC pass. What they often mean is that the records exist and parse, not that they align with the `From:` domain on your live campaigns. A tool can honestly report DKIM pass while signing with `header.d=mail.sendingtool.com`, and unless it checks alignment against your sending domain specifically, the row stays green.
|
||||
|
||||
<Callout title="Trust the header, not the dashboard" variant="warn">
|
||||
A checkmark that reads "DKIM: pass" tells you a signature verified. It doesn't tell you whose domain signed it. The only check that catches broken alignment is opening a delivered message and confirming `header.d=` (or `smtp.mailfrom=`) equals your `From:` domain. If they differ, DMARC is failing no matter how green the panel looks.
|
||||
</Callout>
|
||||
|
||||
This is a common issue we see in support, and it maps onto the two details Google's rules catch people on, which we walk through in [what the bulk sender rules actually ask of cold emailers](/blog/reading-the-bulk-sender-rules/).
|
||||
|
||||
## The SPF 10-lookup limit and other quiet failure modes
|
||||
|
||||
SPF has a hard cap: 10 DNS lookups per evaluation. Every `include:` in your record counts, and popular senders (Google Workspace, a CRM, a warmup or sending tool, a support desk) each add one or more. Stack enough and SPF returns `PermError`, which DMARC treats as a fail. The record still looks fine in a text editor, so this one hides well.
|
||||
|
||||
A few other ways both records read as present but the check breaks:
|
||||
|
||||
| Failure mode | What you see | Why it fails |
|
||||
| --- | --- | --- |
|
||||
| Over 10 SPF lookups | `spf=permerror` | too many `include:` chains |
|
||||
| Third-party DKIM only | `dkim=pass`, `header.d` is the vendor's | signature doesn't align with your From |
|
||||
| SPF-only alignment with forwarding | `spf=fail` after a forward | forwarding breaks the envelope path, and no DKIM to fall back on |
|
||||
| DMARC `p=none` | `dmarc=pass` or `fail` but no enforcement | policy is published but takes no action |
|
||||
| Multiple SPF records | `spf=permerror` | more than one `v=spf1` record is invalid |
|
||||
|
||||
The forwarding row is why relying on SPF alignment alone is fragile. When a message is forwarded, the envelope sender changes and SPF breaks, but a valid DKIM signature survives because it travels with the message body. Aligned DKIM is the more durable of the two. Get both, but if you only fix one, fix DKIM alignment.
|
||||
|
||||
## A 5-minute alignment audit you can run today
|
||||
|
||||
Run this whenever you connect a new mailbox or change a sending tool:
|
||||
|
||||
1. Send one campaign-style message from the mailbox to a Gmail address and one to an Outlook address.
|
||||
2. Open the message, view original or raw source, and find `Authentication-Results`.
|
||||
3. Confirm `dmarc=pass`. If it says `fail`, keep going.
|
||||
4. Check `header.d=` (DKIM) against your `From:` domain. If they match, DKIM aligns.
|
||||
5. Check `smtp.mailfrom=` (SPF) against your `From:` domain. If they match, SPF aligns.
|
||||
6. If neither aligns, add a DKIM key for your domain in the sending tool and republish so it signs with `d=yourdomain.com`.
|
||||
7. Count the `include:` entries in your SPF record. If you're near 10, flatten or drop unused senders before you hit `PermError`.
|
||||
|
||||
Do this per domain, not per tool, since a domain aligned in one product can be unaligned in another. Alignment is the foundation, but it doesn't move you from spam to inbox on its own. That comes from reputation and complaint rates, a separate problem from authentication. We pull that apart in [why delivery rate isn't inbox placement](/blog/delivery-rate-is-not-inbox-placement/) and across the broader [deliverability guide](/deliverability/).
|
||||
|
||||
Warmbly is open source under Apache 2.0, so if you want to see how the sending path handles authentication and signing, the code is at [github.com/warmbly/warmbly](https://github.com/warmbly/warmbly). Fix alignment first. Everything downstream of the inbox assumes it.
|
||||
@@ -3,7 +3,7 @@ import type { MarkdownHeading } from 'astro';
|
||||
import Layout from './Layout.astro';
|
||||
import Icon from '../components/Icon.astro';
|
||||
import CTA from '../components/CTA.astro';
|
||||
import { formatDate, isoDate, readingTimeOf, type BlogPost } from '../lib/blog';
|
||||
import { formatDate, isoDate, readingTimeOf, wordCountOf, type BlogPost } from '../lib/blog';
|
||||
|
||||
interface Props {
|
||||
post: BlogPost;
|
||||
@@ -12,21 +12,29 @@ interface Props {
|
||||
headings?: MarkdownHeading[];
|
||||
}
|
||||
const { post, prev, next, headings = [] } = Astro.props;
|
||||
const { title, description, pubDate, updatedDate, author, tags } = post.data;
|
||||
const { title, description, pubDate, updatedDate, author, tags, cover } = post.data;
|
||||
const reading = readingTimeOf(post);
|
||||
const words = wordCountOf(post);
|
||||
const chapters = headings.filter((h) => h.depth === 2);
|
||||
|
||||
const siteOrigin = (Astro.site ?? new URL('https://warmbly.com')).toString().replace(/\/$/, '');
|
||||
const canonical = new URL(Astro.url.pathname, Astro.site ?? 'https://warmbly.com').toString();
|
||||
// Per-post social/share image: use the post cover when set, else the site card.
|
||||
const shareImage = cover ?? '/og-image.jpg';
|
||||
const shareImageAbsolute = new URL(shareImage, Astro.site ?? 'https://warmbly.com').toString();
|
||||
|
||||
const articleJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
headline: title,
|
||||
description,
|
||||
image: `${siteOrigin}/og-image.png`,
|
||||
image: shareImageAbsolute,
|
||||
inLanguage: 'en',
|
||||
datePublished: pubDate.toISOString(),
|
||||
dateModified: (updatedDate ?? pubDate).toISOString(),
|
||||
articleSection: tags[0] ?? 'Deliverability',
|
||||
keywords: tags.join(', '),
|
||||
wordCount: words,
|
||||
author:
|
||||
author === 'Warmbly team'
|
||||
? { '@id': `${siteOrigin}#org` }
|
||||
@@ -36,12 +44,24 @@ const articleJsonLd = {
|
||||
isAccessibleForFree: true,
|
||||
};
|
||||
|
||||
// BreadcrumbList: Home > Blog > <title>. One of the few schema types Google
|
||||
// still renders as a rich result, and it replaces the raw URL in the SERP.
|
||||
const breadcrumbJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Home', item: `${siteOrigin}/` },
|
||||
{ '@type': 'ListItem', position: 2, name: 'Blog', item: `${siteOrigin}/blog/` },
|
||||
{ '@type': 'ListItem', position: 3, name: title, item: canonical },
|
||||
],
|
||||
};
|
||||
|
||||
const neighbors = [
|
||||
prev ? { label: 'Previously', post: prev } : null,
|
||||
next ? { label: 'Up next', post: next } : null,
|
||||
].filter(Boolean) as { label: string; post: BlogPost }[];
|
||||
---
|
||||
<Layout title={`${title} | Warmbly Blog`} description={description} ogType="article" jsonLd={articleJsonLd}>
|
||||
<Layout title={`${title} | Warmbly Blog`} description={description} ogImage={shareImage} ogType="article" jsonLd={[articleJsonLd, breadcrumbJsonLd]}>
|
||||
<!-- TOP BAR: sticky under the site header, carries the reading progress -->
|
||||
<div class="sticky top-14 z-30 bg-white/90 backdrop-blur-md border-b border-[color:var(--border)]">
|
||||
<div class="container-page py-3 flex items-center justify-between text-[11.5px] font-mono">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import '../styles/global.css';
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import LaunchBanner from '../components/LaunchBanner.astro';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -19,7 +20,7 @@ interface Props {
|
||||
const {
|
||||
title,
|
||||
description = 'Warmbly is the deliverability platform for cold email. Warm your inboxes, send at scale, keep your reputation clean.',
|
||||
ogImage = '/og-image.png',
|
||||
ogImage = '/og-image.jpg',
|
||||
ogType = 'website',
|
||||
jsonLd,
|
||||
noIndex = false,
|
||||
@@ -41,7 +42,7 @@ const organizationJsonLd = {
|
||||
legalName: 'Mindroot Ltd',
|
||||
url: siteOrigin,
|
||||
logo: `${siteOrigin}/web-app-manifest-512x512.png`,
|
||||
image: `${siteOrigin}/og-image.png`,
|
||||
image: `${siteOrigin}/og-image.jpg`,
|
||||
description,
|
||||
email: 'hello@warmbly.com',
|
||||
foundingDate: '2024',
|
||||
@@ -120,9 +121,9 @@ const websiteJsonLd = {
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:image" content={ogImageAbsolute} />
|
||||
<meta property="og:image:secure_url" content={ogImageAbsolute} />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:type" content="image/jpeg" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:height" content="600" />
|
||||
<meta property="og:image:alt" content="Warmbly: the open-source cold email platform" />
|
||||
|
||||
<!-- Twitter / X -->
|
||||
@@ -149,6 +150,7 @@ const websiteJsonLd = {
|
||||
))}
|
||||
</head>
|
||||
<body class="min-h-screen flex flex-col antialiased">
|
||||
{!bare && <LaunchBanner />}
|
||||
{!bare && <Header />}
|
||||
<main class="flex-1">
|
||||
<slot />
|
||||
|
||||
@@ -23,6 +23,11 @@ export function readingTimeOf(post: BlogPost): string {
|
||||
return getReadingTime(post.body ?? '').text;
|
||||
}
|
||||
|
||||
/** Word count from the raw markdown body, for BlogPosting.wordCount schema. */
|
||||
export function wordCountOf(post: BlogPost): number {
|
||||
return getReadingTime(post.body ?? '').words;
|
||||
}
|
||||
|
||||
/** Editorial date for article headers: "June 10, 2026". */
|
||||
export function formatDate(date: Date): string {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
|
||||
@@ -53,7 +53,7 @@ const principles = [
|
||||
{
|
||||
n: '05',
|
||||
title: 'Encrypted by design.',
|
||||
body: 'Application secrets and sensitive payloads use AWS KMS envelope encryption. A per-user DEK. AES-256-GCM at the field level. Plaintext keys only ever live in a Redis cache with a TTL. The repo says it. The migrations enforce it.',
|
||||
body: 'Application secrets and sensitive payloads use AWS KMS envelope encryption. A per-organization DEK. AES-256-GCM at the field level. Plaintext keys only ever live in a Redis cache with a TTL. The repo says it. The migrations enforce it.',
|
||||
},
|
||||
{
|
||||
n: '06',
|
||||
|
||||
@@ -3,6 +3,7 @@ import Layout from '../layouts/Layout.astro';
|
||||
import HeroAtmosphere from '../components/HeroAtmosphere.astro';
|
||||
import Icon from '../components/Icon.astro';
|
||||
import CTA from '../components/CTA.astro';
|
||||
import PillarPosts from '../components/PillarPosts.astro';
|
||||
|
||||
// Every number on this page is a real default from the codebase:
|
||||
// internal/config/constants.go
|
||||
@@ -856,6 +857,15 @@ const numbers = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<PillarPosts
|
||||
title="Deliverability, in more depth"
|
||||
items={[
|
||||
{ kicker: 'Authentication', href: '/blog/spf-dkim-dmarc-alignment-cold-email/', title: 'SPF, DKIM, DMARC alignment: why your green checkmarks can lie' },
|
||||
{ kicker: 'Metrics', href: '/blog/delivery-rate-is-not-inbox-placement/', title: 'Delivery rate is not inbox placement' },
|
||||
{ kicker: 'Thresholds', href: '/blog/complaint-rate-that-gets-you-throttled/', title: 'What complaint, bounce, and spam rate gets you throttled?' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<CTA
|
||||
title="See where your mailbox really stands."
|
||||
description="Connect one and get a full SPF, DKIM, DMARC, and PTR report, plus its current health, in a few minutes."
|
||||
|
||||
@@ -12,7 +12,7 @@ const groups = [
|
||||
['How long does warmup take?', 'Plan on 3–6 weeks for a brand-new mailbox to reach a stable warmed state. We ramp +1 / day from a 10 / day start with a 40 / day ceiling.'],
|
||||
['Do free and premium pools mix?', 'No. Free-pool mailboxes never silently appear in the premium pool. Dedicated workers still participate in the premium pool by default.'],
|
||||
['What gets a mailbox quarantined?', '≥ 3 invalid warmup tokens in 24h, spam score above 50, complaint rate over 0.10%, or spam-folder placement over 20% on a sufficient sample.'],
|
||||
['Can I keep warming after I start cold sending?', 'Yes — and you should. Warmup should keep running even after campaigns begin.'],
|
||||
['Can I keep warming after I start cold sending?', 'Yes, and you should. Warmup should keep running even after campaigns begin.'],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -30,7 +30,7 @@ const groups = [
|
||||
title: 'Inbox & replies',
|
||||
items: [
|
||||
['How accurate is reply detection?', 'Reply detection survives quoted text, signatures, "sent from" footers, and forwarded threads. OOO and unsubscribe replies are classified separately.'],
|
||||
['Can I unify several mailboxes in one view?', 'Yes — that is the whole point. Filters, search and assignment span every connected mailbox.'],
|
||||
['Can I unify several mailboxes in one view?', 'Yes, that is the whole point. Filters, search and assignment span every connected mailbox.'],
|
||||
['Are open and click trackers reliable?', 'Honestly: opens are noisy because of Apple Mail Privacy and image proxies. We surface opens but never use them for deliverability decisions.'],
|
||||
],
|
||||
},
|
||||
@@ -39,7 +39,7 @@ const groups = [
|
||||
title: 'Security',
|
||||
items: [
|
||||
['Where is data stored?', 'AWS regions. us-east-1 by default, eu-west-1 on Grow and Business. Custom residency on Enterprise.'],
|
||||
['How is mailbox auth stored?', 'Refresh tokens are envelope-encrypted with per-user KMS-derived data keys. Decrypted keys are cached briefly in Redis, never persisted to disk.'],
|
||||
['How is mailbox auth stored?', 'Refresh tokens are envelope-encrypted with per-organization KMS-derived data keys. Decrypted keys are cached briefly in Redis, never persisted to disk.'],
|
||||
['Do you have SOC 2?', 'SOC 2 Type II audit is in progress. We can share the bridge letter once the report is issued.'],
|
||||
['Do you sign DPAs?', 'Yes. Email hello@warmbly.com and we will turn it around within 48 hours.'],
|
||||
],
|
||||
@@ -55,10 +55,26 @@ const groups = [
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// FAQPage schema from the groups above. Google deprecated the FAQ rich result,
|
||||
// but the type still feeds answer engines clean question and answer pairs, and
|
||||
// these answers carry the concrete numbers (50/day cap, 600s gap, warmup bands).
|
||||
const faqJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: groups.flatMap((g) =>
|
||||
g.items.map(([q, a]) => ({
|
||||
'@type': 'Question',
|
||||
name: q,
|
||||
acceptedAnswer: { '@type': 'Answer', text: a },
|
||||
})),
|
||||
),
|
||||
};
|
||||
---
|
||||
<Layout
|
||||
title="Frequently Asked Questions | Warmbly"
|
||||
description="Answers to common questions about email warmup, cold email sending limits, deliverability, mailbox connections, security, and billing on Warmbly."
|
||||
jsonLd={faqJsonLd}
|
||||
>
|
||||
<!-- HERO with sticky-nav teaser -->
|
||||
<section class="relative isolate overflow-hidden text-white" style="background: radial-gradient(ellipse 130% 140% at 72% 28%, #0284c7 0%, #0369a1 25%, #075985 50%, #0c4a6e 80%, #0a3d5c 100%);">
|
||||
|
||||
@@ -165,7 +165,7 @@ const softwareJsonLd = {
|
||||
</a>
|
||||
</div>
|
||||
<p class="mt-4 text-[12.5px] text-white/65">
|
||||
14 days, 2 mailboxes, no credit card. Apache 2.0 if you want to self-host.
|
||||
14 days, 3 mailboxes, no credit card. Apache 2.0 if you want to self-host.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -669,6 +669,9 @@ const softwareJsonLd = {
|
||||
See full comparison <Icon name="arrowRight" size={12} />
|
||||
</a>
|
||||
</div>
|
||||
<p class="mt-3 text-center text-[12.5px] text-muted-foreground/85 max-w-xl mx-auto leading-relaxed">
|
||||
The free tier is a limited demo with an unfilled warmup pool. Use it to see what the product does, not to send at volume.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ const guarantees = [
|
||||
|
||||
// --- Security ----------------------------------------------------------------
|
||||
const security = [
|
||||
{ icon: 'lock', t: 'OAuth tokens encrypted at rest', d: 'AES-256-GCM with a per-user data key wrapped by AWS KMS. Plaintext lives only in a TTL-bounded cache during active use.' },
|
||||
{ icon: 'lock', t: 'OAuth tokens encrypted at rest', d: 'AES-256-GCM with a per-organization data key wrapped by AWS KMS. Plaintext lives only in a TTL-bounded cache during active use.' },
|
||||
{ icon: 'key', t: 'API keys are opaque blobs', d: 'CRM, automation and notification keys are never serialized back to the API. The dashboard sees only public display fields.' },
|
||||
{ icon: 'link', t: 'Inbound URLs are per-organization', d: 'A leaked URL touches one organization. Rotating the secret invalidates the old path immediately.' },
|
||||
{ icon: 'shield', t: 'SSRF-guarded by default', d: 'Outbound webhook targets are HTTPS-only and blocked from obvious internal addresses. Self-hosted can opt into unsafe URLs.' },
|
||||
@@ -630,7 +630,7 @@ X-Warmbly-Signature: t=1748443269,v1=9f2a…c1b7
|
||||
Credentials are sealed, not stored.
|
||||
</h2>
|
||||
<p class="mt-5 text-[15px] text-foreground/70 leading-relaxed max-w-md">
|
||||
Integration credentials use the same encryption envelope as mailbox OAuth tokens. Per-user data encryption keys are wrapped by AWS KMS, and plaintext keys are never written to disk.
|
||||
Integration credentials use the same encryption envelope as mailbox OAuth tokens. Per-organization data encryption keys are wrapped by AWS KMS, and plaintext keys are never written to disk.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -248,6 +248,12 @@ const headers = ['Starter', 'Grow', 'Business', 'Enterprise'];
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p class="mt-9 text-center text-[13.5px] text-foreground/65 max-w-2xl mx-auto leading-relaxed">
|
||||
Want to try it first? The free tier runs the core product so you can see how sending, the inbox and
|
||||
campaigns work. It is limited, and its warmup pool is not filled, so treat it as a way to evaluate
|
||||
Warmbly, not a production sender. Warmup performs on a paid plan, where the premium pool is full.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -362,7 +368,7 @@ const headers = ['Starter', 'Grow', 'Business', 'Enterprise'];
|
||||
['Is warmup metered separately?', 'No. Warmup is unlimited on every paid plan and runs against the premium pool.'],
|
||||
['Is there a per-seat charge?', 'No. Add as many teammates as you want. Roles and audit log unlock on Business.'],
|
||||
['Annual billing?', 'Yes, with 20% off. Toggle is at the top of this page.'],
|
||||
['What is the free trial?', '14 days, 2 mailboxes. No credit card. Converts to your chosen plan after.'],
|
||||
['What is the free trial?', '14 days, 3 mailboxes. No credit card. Converts to your chosen plan after.'],
|
||||
['Refund policy?', 'Full refund inside 14 days of a paid purchase if the platform did not work for you.'],
|
||||
['Startup pricing?', '50% off Grow for one year for pre-seed and seed companies on YC, Antler, EF, Techstars or similar.'],
|
||||
].map(([q, a]) => (
|
||||
@@ -423,7 +429,7 @@ const headers = ['Starter', 'Grow', 'Business', 'Enterprise'];
|
||||
Start sending in five minutes.
|
||||
</h2>
|
||||
<p class="mt-5 text-[15.5px] md:text-[17px] text-white/80 max-w-md mx-auto">
|
||||
14 days free · 2 mailboxes · no credit card.
|
||||
14 days free · 3 mailboxes · no credit card.
|
||||
</p>
|
||||
<div class="mt-10 flex flex-wrap items-center justify-center gap-3">
|
||||
<a
|
||||
|
||||
@@ -3,6 +3,7 @@ import Layout from '../layouts/Layout.astro';
|
||||
import HeroAtmosphere from '../components/HeroAtmosphere.astro';
|
||||
import Icon from '../components/Icon.astro';
|
||||
import CTA from '../components/CTA.astro';
|
||||
import PillarPosts from '../components/PillarPosts.astro';
|
||||
|
||||
// Single-send trace
|
||||
const trace = [
|
||||
@@ -410,6 +411,15 @@ const faq = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<PillarPosts
|
||||
title="Sending, in more depth"
|
||||
items={[
|
||||
{ kicker: 'Limits', href: '/blog/how-many-cold-emails-per-day-per-mailbox/', title: 'How many cold emails can you actually send per day?' },
|
||||
{ kicker: 'Planning', href: '/blog/how-many-mailboxes-and-domains-cold-email/', title: 'How many mailboxes and domains do you need?' },
|
||||
{ kicker: 'Budgets', href: '/blog/mailbox-budgets-not-server-settings/', title: 'Your send limit is a mailbox budget, not a server setting' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<CTA
|
||||
title="Connect a mailbox. Send your first campaign today."
|
||||
description="OAuth in 30 seconds. Worker assignment is automatic."
|
||||
|
||||
@@ -106,7 +106,7 @@ const sections = [
|
||||
<section id="limits" class="scroll-mt-24">
|
||||
<h2>07. Usage limits and increase requests</h2>
|
||||
<p>
|
||||
Every plan operates under usage limits — mailboxes, campaigns, team members, contacts, daily sending volume, and other capacity controls described in the workspace's billing area. Where marketing language describes a resource as "unlimited," that means there is no plan-tier cap, but a product-wide hard ceiling still applies. Hard ceilings exist to protect shared infrastructure, deliverability for other customers, and our compliance with mailbox-provider policies, and are necessary even on premium and enterprise tiers.
|
||||
Every plan operates under usage limits: mailboxes, campaigns, team members, contacts, daily sending volume, and other capacity controls described in the workspace's billing area. Where marketing language describes a resource as "unlimited," that means there is no plan-tier cap, but a product-wide hard ceiling still applies. Hard ceilings exist to protect shared infrastructure, deliverability for other customers, and our compliance with mailbox-provider policies, and are necessary even on premium and enterprise tiers.
|
||||
</p>
|
||||
<p>
|
||||
You may request an increase to any limit through the in-app limit-request form in your workspace settings. Each request is reviewed by our team. We may approve, partially approve, decline, or defer any request <strong>at our sole discretion</strong>, with or without further explanation, and we are under no obligation to grant any particular increase regardless of plan tier or contractual commitment. Reasons we may decline include, without limitation: insufficient sending reputation, prior abuse signals, deliverability risk to shared pools, fraud-prevention concerns, infrastructure capacity, and compliance considerations.
|
||||
|
||||
@@ -39,7 +39,7 @@ const posture = [
|
||||
},
|
||||
{
|
||||
label: 'Encryption at rest via AWS KMS',
|
||||
note: 'Per-user data keys, AES-256-GCM application layer.',
|
||||
note: 'Per-organization data keys, AES-256-GCM application layer.',
|
||||
state: 'shipped',
|
||||
},
|
||||
{
|
||||
@@ -64,12 +64,12 @@ const flow = [
|
||||
{
|
||||
n: '01',
|
||||
title: 'AWS KMS',
|
||||
body: 'Customer Master Key lives in KMS and never leaves it. KMS generates a 32-byte data encryption key for each user on first use.',
|
||||
body: 'Customer Master Key lives in KMS and never leaves it. KMS generates a 32-byte data encryption key for each organization on first use.',
|
||||
tag: 'Root of trust',
|
||||
},
|
||||
{
|
||||
n: '02',
|
||||
title: 'Per-user DEK',
|
||||
title: 'Per-organization DEK',
|
||||
body: 'The plaintext DEK is returned briefly to the application. The encrypted DEK blob comes back in the same call and is the only long-term copy.',
|
||||
tag: 'AES-256',
|
||||
},
|
||||
|
||||
@@ -3,6 +3,8 @@ import Layout from '../layouts/Layout.astro';
|
||||
import HeroAtmosphere from '../components/HeroAtmosphere.astro';
|
||||
import Icon from '../components/Icon.astro';
|
||||
import CTA from '../components/CTA.astro';
|
||||
import WarmupPoolNotice from '../components/WarmupPoolNotice.astro';
|
||||
import PillarPosts from '../components/PillarPosts.astro';
|
||||
|
||||
// All numbers below come from the Warmbly codebase / CLAUDE.md
|
||||
// (internal/scheduler/warmup_scheduler.go, internal/repository/pg_warmup.go,
|
||||
@@ -518,6 +520,24 @@ for (let wk = 0; wk < 5; wk++) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
SELF-HOST NOTICE · the shared pool is a cloud feature
|
||||
============================================================ -->
|
||||
<section class="border-b border-[color:var(--border)] py-14 md:py-16">
|
||||
<div class="container-page">
|
||||
<div class="max-w-2xl mb-8">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Running this yourself</div>
|
||||
<h2 class="text-[26px] md:text-[36px] font-semibold tracking-[-0.025em] leading-[1.08] text-heading">
|
||||
The pool needs scale. That is what the cloud provides.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15px] text-foreground/70 leading-relaxed">
|
||||
Warmbly is open source, so you can self-host all of it. The one part that does not travel is the shared warmup pool, because a pool only works once enough vetted mailboxes are in it.
|
||||
</p>
|
||||
</div>
|
||||
<WarmupPoolNotice class="max-w-3xl" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
DEFAULTS · clean 2-column spec sheet with big mono values
|
||||
============================================================ -->
|
||||
@@ -608,6 +628,15 @@ for (let wk = 0; wk < 5; wk++) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<PillarPosts
|
||||
title="How warmup actually behaves"
|
||||
items={[
|
||||
{ kicker: 'Warmup', href: '/blog/how-email-warmup-actually-works/', title: 'How email warmup actually works, ramp curve and all' },
|
||||
{ kicker: 'Policy', href: '/blog/quarantine-early/', title: 'Quarantine early: how we protect shared warmup pools' },
|
||||
{ kicker: 'Recovery', href: '/blog/getting-a-mailbox-back-into-warmup-pool/', title: 'Getting a burned mailbox back: a recovery playbook' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<CTA
|
||||
title="Connect a mailbox. Start warming today."
|
||||
description="OAuth in 30 seconds. First warmup send in under a minute."
|
||||
|
||||