Django 的 send_mail 是同步阻塞调用,会导致视图卡住、HTTP 超时、worker 占满等问题;应改用 threading(轻量)或 Celery+Redis(生产),并优先使用 EmailMessage 支持 HTML、附件等功能。

同步 send_mail 会卡住视图,别直接用
Django 的 send_mail 是纯同步阻塞调用,SMTP 连接、TLS 协商、认证、发信全程在主线程跑完才返回。用户注册页提交后等 2–3 秒白屏,不是体验问题,是架构缺陷。
常见错误现象包括:
- HTTP 请求超时(Nginx 默认 60s,但用户早关页面了)
-
ConnectionRefusedError或TimeoutError直接抛到视图,触发 500 页面 - 高并发下 uwsgi/gunicorn worker 被占满,新请求排队
根本原因:邮件不是核心业务路径,但它绑死了 Web 请求生命周期。解耦的第一步,就是不让它出现在 request/response 链路里。
轻量场景用 threading.Thread 就够了
如果你只是想让用户注册后「秒回响应」,且日均发信量 threading.Thread 是最简单、最可控的方案——不引入新组件,不改部署结构,几行代码就能落地。
立即学习“Python免费学习笔记(深入)”;
实操要点:
提供使用 django-unfold (v0.56+) 构建高级 Django 管理界面的专业指导。当用户提及 django-unfold、unfold admin 等内容时使用此技能。
- 必须提前把所有依赖数据转成 JSON-safe 类型:
user.email、user.username、渲染好的 HTML 字符串,不能传request或未序列化的User实例 - 别用
ThreadPoolExecutor:单次发信调度开销 > 实际发送耗时,反而拖慢 - 线程内不捕获异常:SMTP 失败不会报给 Django 日志,得手动加
try/except+logging.error - 示例写法:
from threading import Thread from django.core.mail import send_mail def send_async_mail(subject, message, from_email, recipient_list): Thread( target=send_mail, args=(subject, message, from_email, recipient_list), kwargs={'fail_silently': False} ).start()
Celery + Redis 是生产环境唯一合理选择
一旦发信量上升、或需要重试、定时、监控、失败告警,threading 就撑不住了。Celery 不是“更高级”,而是补上了同步模型缺失的关键能力:任务持久化、失败重入队、分布式 worker、执行追踪。
容易踩的坑:
-
CELERY_BROKER_URL和CELERY_RESULT_BACKEND必须用同一个 Redis DB,否则 task 状态查不到 - Windows 开发必须加
-P eventlet启动 worker,否则报BlockingIOError -
EMAIL_HOST_PASSWORD是「应用专用密码」,不是邮箱登录密码;Gmail/Outlook 必须开两步验证再生成 - ECS 云服务器默认封 25 端口,
EMAIL_PORT改 587 +EMAIL_USE_TLS = True,或 465 +EMAIL_USE_SSL = True,二者不能同时为True
关键配置示例(settings.py):
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = 'smtp.gmail.com' EMAIL_PORT = 587 EMAIL_USE_TLS = True EMAIL_HOST_USER = 'your@gmail.com' EMAIL_HOST_PASSWORD = 'your_app_specific_password' CELERY_BROKER_URL = 'redis://:password@localhost:6379/1' CELERY_RESULT_BACKEND = 'redis://:password@localhost:6379/1'
EmailMessage 比 send_mail 多的不只是 HTML
send_mail 只能发纯文本,连抄送(cc)、密送(bcc)、附件、内嵌图片都做不到。真正要用的接口是 EmailMessage,它才是 Django 邮件系统的底层入口。
典型需求对应写法:
- 发 HTML 邮件:
msg.content_subtype = 'html' - 加附件:
msg.attach_file('/path/to/file.pdf') - 内嵌图片:
msg.attach('logo.png', img_data, 'image/png')+ HTML 中用cid:logo.png - 多收件人+抄送:
to=['a@x.com'], cc=['b@x.com'], bcc=['c@x.com']
注意:Celery task 函数里不能用 render_to_string 直接读模板——模板路径可能因 worker 环境不同而失效,建议提前渲染好字符串再传入 task。
线程方案看似简单,但没失败重试、无执行记录、无法横向扩展;Celery 看似重,但只要 Redis 在,任务就不丢。真正难的从来不是怎么发,而是「发丢了怎么办」和「发慢了怎么查」——这两个问题,只有 Celery 能答。

















