需求很單純:客戶按下「發佈」,系統就把新文章寄給每一位訂閱者。天真版三行就能寫完——抓出所有訂閱者、跑一個迴圈、全部送出去。問題是,這三行同時踩了三個雷。
差別不在寄什麼,而在「怎麼送」——爆送賭上帳號,節流保住信譽。
天真版踩的三個雷
- 擋住發文請求。在發文當下同步把幾百封寄完,客戶會盯著轉圈圈等到天荒地老。
- 外洩收件人。想省事把所有人塞進同一封的 BCC?只要有一個環節把收件人攤開,就洩漏了整份訂閱名單。
- 被 Gmail 鎖。用一般 Gmail/SMTP 帳號在幾秒內狂送一大批,很容易被判濫發——輕則進垃圾桶,重則暫停帳號。
我們的做法
四個原則,把三個雷各個拆開:背景送、不擋請求;一人一封、單一收件人;逐封節流、串行不併發;先標記、只廣播一次。
// 發文 hook:先標記已寄,再「背景」慢送(不 await)
await markSent(post, { req });
void broadcastNewPost(post); // 射後不理,發文立即回應
async function broadcastNewPost(post) {
const subs = await findActiveSubscribers();
for (const s of subs) {
await sendEmail({ to: s.email, ...render(post, s.name) }); // 一人一封
await sleep(DELAY_MS); // 逐封節流(例:4s)
}
}
一人一封、單一收件人:任何一封信裡都看不到別的訂閱者。
眉角
- 背景任務是「行程內記憶體佇列」。容器中途重啟,沒送完的會漏;所以「先標記已寄」,重啟後不會整批重寄——寧可少數人漏收,也不要全部再被轟一次。
- 節流讓一篇可能數分鐘才送完,這是刻意的取捨,不是慢。
- 真要規模化再換寄信服務(Resend / SES)+佇列;對剛起步的部落格,「慢慢送」已經夠穩又夠省。
帶得走的一句話
「通知所有訂閱者」的天真版之所以危險,是它把回應速度、隱私、寄件信譽全賭在同一個迴圈上。拆開來:背景送保住體驗、一人一封保住隱私、逐封節流保住帳號。慢,反而是這裡最安全的設計。