Password reset email not arriving? Causes and fixes
The password reset email never arrived. How to tell whether the app sent it, the provider refused it or it went to spam, and the practices that make reset emails arrive every time.
Updated
View as MarkdownThe password reset email is the most urgent message your system sends: the person is on the login screen, waiting, locked out. If it does not arrive within seconds, they give up, open a ticket or go to a competitor. And you rarely find out.
Where the email may have stopped#
Follow the message's path, from your system to the inbox:
- The system never sent it. An error swallowed in a
try/catch, a stopped job queue, no user with that email. - The sending service refused it. Sender outside a verified domain, daily limit, address on the suppression list from an old bounce.
- The destination server refused or deferred it. A
5xxanswer (refused) or4xx(try later). - It was delivered, but to spam or a tab the person does not check.
- It arrived, but the link does not work: expired, already used (sometimes by a filter that visited it first) or pointing to the wrong domain.
Step-by-step diagnosis#
- Look for the request in your app's logs, by the person's email and the time. If there is no send record, the problem is before the email.
- Look for the message in the sending service. A good service shows the timeline: accepted, sent, delivered or refused, with the destination server's answer.
- Read the reply code.
550 5.1.1is an address that does not exist;550 5.7.26or5.7.515is missing authentication;421and451are deferrals. The full list is in SMTP error codes explained. - If it was delivered, ask the person to check spam and check authentication by opening a copy with Show original in Gmail.
The most common causes, one by one#
The send error was swallowed#
Code like this is common, and it is the "silent failure" nobody sees:
try {
await sendPasswordEmail(user);
} catch {
// nothing here
}Log the error with its code and request ID, and alert when the failure rate rises. If sending goes through a queue, watch for stuck and failed jobs.
The sender is not authenticated#
A password email leaving through the hosting server's mail(), a random SMTP server or with a From on a domain without SPF, DKIM and DMARC lands in spam or is refused, especially at Outlook and Hotmail. See How to set up SPF, DKIM and DMARC.
The address is suppressed#
If an old email to that person bounced with "user unknown", the sending service may have put the address on the suppression list to protect your reputation. If the person fixed their mailbox, remove the address from the list and send again.
The provider is deferring#
A new domain or a sudden spike makes providers answer "try later" (421, 451). The service retries on its own, but the email arrives late, and for a password reset late is almost the same as never. Warm the domain up gradually and keep transactional apart from marketing.
The link arrives broken#
- Wrong domain: the link was built from an internal request host or
localhost. Use a configured base URL. - Single use spent by a filter: some corporate filters visit links before the person. Prefer a link that opens a page with a "Set new password" button, where only submitting the form uses the token.
- Lifetime too short: 30 to 60 minutes is usually a good balance, with a way to request another on the same screen.
Good practices for the reset email#
- Send it right away, on a transactional-only path, never in the middle of a campaign.
- Short, clear subject: "Reset your Example password".
- A text part and the link written out besides the button.
- State the lifetime and what to do if the person did not ask for it.
- No ads in this email.
- A generic message on screen ("if an account exists for this email, we sent the link"), so you do not reveal who has an account.
- An idempotency key when the system may repeat the request, so the person does not get three emails and use the wrong link.
- A per-person limit (for example, one request a minute) to prevent abuse.
In the most used frameworks#
| Framework | Where the reset email comes from | Guide |
|---|---|---|
| Laravel | Password::sendResetLink and the ResetPassword notification |
Laravel |
| Django | PasswordResetView with EMAIL_BACKEND |
Django |
| Supabase | resetPasswordForEmail with the project's SMTP |
Supabase |
| Firebase | sendPasswordResetEmail or generatePasswordResetLink |
Firebase |
| WordPress | "Lost your password?" with wp_mail |
WordPress |
Where Couryo fits#
At Couryo, the API only answers success once the email is queued, and each message has a timeline with the destination server's original answer and a plain-words explanation ("Hotmail refused because the domain has no DMARC; fix it like this"). Transactional stays apart from marketing, and a pause for too many bounces never hits the password email first. See Sending email.
FAQ#
Why does the password reset reach Gmail but not Hotmail?#
Microsoft requires full authentication from bulk senders and is stricter with domains without DMARC. Check the sender's SPF, DKIM and DMARC and the reply code when the message bounces.
The customer says they did not get it, but the system shows "sent". Now what?#
"Sent" in your system usually means "handed to the sending service". Find the message in the service and see what the destination server answered. If it was accepted (250), it is in spam or a tab; if it was refused, the code explains why.
Can I resend automatically when it does not arrive?#
Resending after a temporary error is fine, and a good service already does it. For the person, offer a "resend" button with a minimum interval and use one idempotency key per request, so emails do not multiply.
How long should the reset email take?#
Seconds. If it takes minutes, the destination provider is deferring or your queue is stuck; both show in the message's timeline.