Bulk emails sending too fast causing gmail to block account. Is there a way to rate limit?

I talked with google customer service the other day and they assured me that it’s possible for an account to send to up to 2000 emails per day. Today I tried to send a bulk email to about 1300 class participants. But it said it successfully sent to ~120 of them, before it errored out for the rest. The logs show an SMTP error 421 “try again later” (see below). I contacted google support and confirmed the sending account itself is not blocked for exceeding the daily limit, and they suggested it was probably sending too fast, and that you’re only allowed to connect once per second. They then showed me how to look up a log and it indeed seemed to be sending at a rate of about 2 per second.

So my question is: is there a way to slow down mail sending so that it doesn’t get blocked?

My secondary question is, when an email sending errors out like this, is there any way to pick it back up again, or do I just have to accept that ~120 people will get the same message again? (if I can find a way to resolve this issue.

[36;1mlms-worker_1     |[0m Traceback (most recent call last):
[36;1mlms-worker_1     |[0m   File "/openedx/venv/lib/python3.8/site-packages/celery/app/trace.py", line 412, in trace_task
[36;1mlms-worker_1     |[0m     R = retval = fun(*args, **kwargs)
[36;1mlms-worker_1     |[0m   File "/openedx/venv/lib/python3.8/site-packages/celery/app/trace.py", line 704, in __protected_call__
[36;1mlms-worker_1     |[0m     return self.run(*args, **kwargs)
[36;1mlms-worker_1     |[0m   File "/openedx/venv/lib/python3.8/site-packages/edx_django_utils/monitoring/internal/code_owner/utils.py", line 179, in new_function
[36;1mlms-worker_1     |[0m     return wrapped_function(*args, **kwargs)
[36;1mlms-worker_1     |[0m   File "/openedx/edx-platform/lms/djangoapps/bulk_email/tasks.py", line 334, in send_course_email
[36;1mlms-worker_1     |[0m     raise send_exception  # pylint: disable=raising-bad-type
[36;1mlms-worker_1     |[0m   File "/openedx/edx-platform/lms/djangoapps/bulk_email/tasks.py", line 581, in _send_course_email
[36;1mlms-worker_1     |[0m     connection.send_messages([email_msg])
[36;1mlms-worker_1     |[0m   File "/openedx/venv/lib/python3.8/site-packages/django/core/mail/backends/smtp.py", line 110, in send_messages
[36;1mlms-worker_1     |[0m     sent = self._send(message)
[36;1mlms-worker_1     |[0m   File "/openedx/venv/lib/python3.8/site-packages/django/core/mail/backends/smtp.py", line 126, in _send
[36;1mlms-worker_1     |[0m     self.connection.sendmail(from_email, recipients, message.as_bytes(linesep='\r\n'))
[36;1mlms-worker_1     |[0m   File "/opt/pyenv/versions/3.8.6/lib/python3.8/smtplib.py", line 871, in sendmail
[36;1mlms-worker_1     |[0m     raise SMTPSenderRefused(code, resp, from_addr)
[36;1mlms-worker_1     |[0m smtplib.SMTPSenderRefused: (421, b'4.7.0 Try again later, closing connection. (MAIL) r186sm3113766qkf.128 - gsmtp',

@oedx, the platform already has a setting variable which is BULK_EMAIL_RETRY_DELAY_BETWEEN_SENDS 0.2 (second) by default, so I guess just by changing it to 2, it might work as you are expecting.

As for your other question, I am not sure about the exact implementation1, it seems that the function/task is by design tolerant for errors, but not sure, about the exact error above, since the error name is not handled but the error code is! 2.

But anyway, please try to set the setting, and let me know if the problem still occurs.

1 Like

This doesn’t seem to have worked…

I’m using tutor so I created a plugin to set it, and then I grepped the config to confirm it was set

grep -r BULK_EMAIL_RETRY_DELAY_BETWEEN_SENDS /home/tutor/.local/share/tutor/env/apps/openedx/config/
/home/tutor/.local/share/tutor/env/apps/openedx/config/lms.env.json:  "BULK_EMAIL_RETRY_DELAY_BETWEEN_SENDS": "2",

However I see it still sending emails at a rate > 1/s

Was it maybe that it needed to just be
“BULK_EMAIL_RETRY_DELAY_BETWEEN_SENDS”: 2
instead of
“BULK_EMAIL_RETRY_DELAY_BETWEEN_SENDS”: “2”
? (i.e. no quotes on the 2) (I want to make sure I get it right before I spam a bunch of people again…)

Since you are changing a json file, it would depends on the parser. somtimes it doesn’t matter.
However to be exact use it without qoutation, hence this how the deafult value is defined as shown here

Also there is another env variable that might intrest you, which is the number of emails per task, which is by defualt is 500 as defined BULK_EMAIL_EMAILS_PER_TASK here `
If you set for one, then if an email failed to sent, then I would assume you would be able to debug or in case of fauier it would be easier to recover?

I did
“BULK_EMAIL_RETRY_DELAY_BETWEEN_SENDS”: 2
but it still sent 94 emails in 86 seconds before getting blocked, so I think it’s still not working. (I can see other values have no quotes around numbers, so I do think that’s probably the right syntax).
I continue to see

smtplib.SMTPSenderRefused: (421, b'4.7.0 Try again later, closing connection.

in the logs.

I feel like maybe that BULK_EMAIL_RETRY_DELAY_BETWEEN_SENDS isn’t the right setting because maybe it’s only the RETRY delay, as opposed to just the normal sending delay?

@oedx,

Yeah you are right, the 2 second is a delay when an error or exaception is thrown, so the probelm here is that mostly that the type of error/exception is not handled SMTPSenderRefused. i.e. the error is not flagged as retryable though it should.
I have pushed a PR to fix it fix: make bulk_email send email task handles a retryable exception by ghassanmas · Pull Request #29080 · edx/edx-platform · GitHub