Nobody unsubscribes from the message that annoyed them. They unsubscribe from the fourth one, and they attribute the annoyance to your business rather than to your send schedule. By the time it shows in your numbers, the damage is a year old.
Quiet hours
The widely used standard is 8am to 9pm in the recipient's local time zone. That is a floor, not a target — and the time zone part is where implementations quietly fail.
// The recipient's zone, not your server's. This is the bug everyone ships.export function withinSendWindow(now: Date, timeZone: string) { const hour = Number( new Intl.DateTimeFormat("en-US", { timeZone, hour: "numeric", hour12: false, }).format(now), ); return hour >= 8 && hour < 21;} // No zone on file? Derive it from the area code, and default to holding// the message until a safe hour rather than guessing wrong.Queue, do not drop
A message that fails the quiet-hours check should be held until the window opens, not discarded. Silently dropping reminders is how someone misses an appointment because of your compliance logic.
Frequency caps that actually hold
Enforce the cap at the send layer, not in each campaign's logic. Otherwise three well-behaved campaigns combine into one badly-behaved week.
- Marketing: at most 2–4 per month. Most small businesses should be at the low end of that.
- Transactional: as needed — but a reminder, a nudge and a follow-up for one appointment is already three messages. Count them.
- Hard daily cap across everything. One message per person per day, except for genuine two-way conversation.
- Never two campaigns in the same 48 hours. Stagger them, and let the send layer enforce it.
export async function canSend(customerId: string, kind: "marketing" | "transactional") { const recent = await messagesSince(customerId, days(30)); if (recent.some((m) => isToday(m.sentAt))) return false; // daily cap if (kind === "transactional") return true; // expected sends const marketing = recent.filter((m) => m.kind === "marketing"); if (marketing.length >= 4) return false; // monthly cap if (marketing.some((m) => hoursSince(m.sentAt) < 48)) return false; return true;}Watch the leading indicator
Opt-out rate per send is your early-warning system. A stable low rate means your cadence is fine. A rate that climbs across three consecutive sends means you are over-messaging, and it will keep climbing until you cut back. Treat a rising opt-out rate as an outage, not a statistic.
Track it alongside your revenue numbers so the trade-off is visible in one place — measuring ROI covers the reporting.
The cap is not a compliance chore. It is what keeps the channel worth having in year three.
Next step
Generate a tagged link for whatever you send next with the UTM builder, see what this looks like in your industry, or compare the services that can send it on the providers page.