因为Django默认send_mail()每次新建SMTP连接,100封邮件需100次TCP握手、TLS协商与认证,耗时激增且易触发邮箱限流;应改用多线程+每线程独享长连接(starttls后保持、显式quit),避免共享非线程安全的SMTP实例。

为什么直接用 send_mail() 批量发邮件会卡住?
因为 Django 默认的 send_mail() 每次都新建 SMTP 连接,发一封建一次、关一次。100 封邮件 = 100 次 TCP 握手 + TLS 协商 + 认证,实际耗时可能翻 5–10 倍,还容易触发邮箱服务商的限流(比如 Gmail 返回 421 4.7.0 Temporary System Problem)。
真正要提速,得复用连接——不是靠“多线程”本身,而是让多个线程能从一个共享的、已认证的 SMTP 连接池里取连接。
- 别自己写线程安全的 SMTP 池:Python 的
smtplib.SMTP实例**不是线程安全的**,不能在多线程间共享单个实例 - 每个线程应持有一个独立的、长连接的
smtplib.SMTP实例(启用smtp.starttls()后保持打开) - Django 的
EmailBackend不支持连接池,必须绕过它,手管连接生命周期
怎么用 threading.Thread + 独立 SMTP 实例发批量邮件?
核心是「每个线程初始化自己的 SMTP 连接,发完再显式 quit()」。不依赖 Django 邮件后端,避免中间层开销和连接复用限制。
示例关键逻辑:
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
<p>def send_batch_chunk(emails, smtp_host, smtp_port, username, password):</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill11526" title="Python Hardener"><img
src="https://img.php.cn/upload/skill/001/246/273/6adaf20dbae7c507.jpg" alt="Python Hardener" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill11526" title="Python Hardener" class="overflowclass">Python Hardener</a>
<p class="overflowclass">通过完整的安全审查、代码整洁重构、健壮的错误处理、正确的日志记录以及完整的文档字符串覆盖,强化 Python 脚本。</p>
</div>
<a rel="nofollow" href="/xiazai/skill11526" title="Python Hardener" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div><h1>每个线程独占一个连接</h1><pre class="brush:php;toolbar:false;">server = smtplib.SMTP(smtp_host, smtp_port)
server.starttls()
server.login(username, password)
for to_email, subject, body in emails:
msg = MIMEMultipart()
msg["From"] = username
msg["To"] = to_email
msg["Subject"] = subject
msg.attach(MIMEText(body, "plain"))
server.send_message(msg)
server.quit() # 必须调用,否则连接泄漏
分片后启动线程
chunks = [emails[i:i+10] for i in range(0, len(emails), 10)] threads = [] for chunk in chunks: t = threading.Thread(target=send_batch_chunk, args=(chunk, "smtp.gmail.com", 587, "a@gmail.com", "app_pass")) t.start() threads.append(t) for t in threads: t.join()
- 每批 ≤ 10 封较稳妥:太多易被识别为群发,触发风控;太少则线程开销占比高
- Gmail 必须用 App Password,且需开启「两步验证」后生成,普通密码会报
SMTPAuthenticationError - 不要在循环里反复
server.login()—— 登录一次即可发多封
遇到 Connection unexpectedly closed 或超时怎么办?
这是 SMTP 连接空闲超时(如腾讯企业邮箱默认 5 分钟断连),或线程中未捕获异常导致连接没 quit() 就退出,下次复用时出错。
- 给
server.send_message()加try/except smtplib.SMTPServerDisconnected,捕获后重连再发当前邮件 - 所有 SMTP 操作必须包在
try/finally里,确保server.quit()或server.close()执行 - 避免用
time.sleep()控制频率:不如用线程数限流(例如最多开 3 个线程),更稳定 - 若用阿里云邮件推送,注意其要求
MAIL FROM必须是已验证域名下的邮箱,否则静默失败
要不要换 aiosmtplib 或 Celery?
如果并发量真大(比如每小时万级)、且项目已用 asyncio,aiosmtplib 能省更多资源;但对大多数 Django 后台任务,纯线程 + 显式连接管理更直白、调试方便、无事件循环耦合风险。
Celery 适合「异步解耦」,不是「提速刚需」:它只是把发信逻辑挪到 worker 进程,若 worker 还是单连接串行发,速度没变;只有配合连接池或并发 worker 才有意义。而多数中小项目,直接在 management command 里跑多线程更轻量。
真正容易被忽略的是 DNS 解析和证书验证开销——首次 smtplib.SMTP() 初始化时会做 DNS 查询和 TLS 握手,建议在主线程预热一次连接并测通,再分发子线程任务。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










