Test your password reset flow before launch

What breaks password reset flows, ranked — and how to verify the whole loop: the email arrives, the one-time link works, the new password logs you in.

When a real user clicks “Forgot password?” they are, by definition, already locked out and slightly annoyed. If anything in the reset flow is broken, they have no fallback — they can’t just log in instead. And reset is the flow most likely to be broken on launch day, because it’s the hardest one to test: it isn’t a single page, it’s a loop that leaves your app, travels through an email server and a DNS configuration and a recipient’s spam filter, and comes back through a one-time tokenized link. Every free pre-launch checker skips it — static audits confirm the page loads and returns 200; none of them requests a reset, waits for the email, or follows the link. In a June 2026 audit of 49 fresh web launches, 38 (78%) shipped with at least one critical verified finding on day one. (That audit walks public pages, so it can’t tell you how many reset flows specifically broke — but it tells you launch-day code ships broken far more often than founders expect.)

The safe way to test a password reset flow before launch is to test the whole loop, in order: request a reset, confirm the email arrives, follow the link, set a new password, and log in with it. The request form working tells you almost nothing — reset flows break in the email and token steps, and those only show up when you complete the loop. This piece is the list of what breaks, ranked, and how to verify each one.

Why static checkers and a quick self-test miss this

A static audit confirms the reset page exists and returns 200. It doesn’t submit the form, wait for an email, open the link, or try the new password. A password reset is a multi-step flow across systems you don’t fully control, and flow testing is a different problem from page testing.

Testing it casually misses the same way. You request a reset from your own browser to your own warm inbox — the address that’s already whitelisted, already not spam-foldered — and the link is fresh because you click it ten seconds later. Real users hit the cold path: the brand-new sending domain with no reputation, the aggressive corporate spam filter, the link they open an hour later when the token may already have expired. The good news is the same as with signup flows, checkout flows, and contact forms: the surface is small and bounded. A handful of things break password resets. You can know all of them.

The things that break password reset flows

In rough order of how often they show up in founder reports and in our own audits:

  1. The reset email never arrives. This is the big one, by a wide margin — the same failure that kills signup verification and contact-form notifications, pointed at the one email a locked-out user must receive. Why it happens: the host disabled the default mail function, the domain is missing valid SPF/DKIM/DMARC records so the message is rejected or spam-foldered, the host throttled outbound mail, or — the vibe-coding classic — the dev build logged the reset link to the console instead of sending it, and nobody wired a real authenticated mail provider before launch. Verify it: request a reset on the live site and confirm the email lands in the inbox and the spam folder. If it’s in spam, fix your email authentication records. If it’s nowhere, your send path was never real.

  2. The reset link is dead or points at the wrong place. The email arrives, but the link 404s, points at localhost, or carries a base URL baked in at build time that doesn’t match production. Why it happens: the reset URL is assembled from an environment variable or a hardcoded host that was right in dev and wrong in prod; or the token query parameter gets mangled in the email HTML (an unescaped & can truncate the URL and drop token= entirely). Verify it: open the link from the actual email — not a copy-paste — on a device that isn’t your dev machine. It should land on a real set-a-new-password page with the token intact in the URL.

  3. The token doesn’t expire, or isn’t single-use. The link still works 24 hours later, or it works a second time after you’ve already reset once. This is a security failure, not a UX nit: a reset link that lives forever in an old email is a standing account-takeover vector. Why it happens: the token was stored without an expiry, or “mark used” was never wired. Verify it: reset once, then click the same link again — it must be refused. Then request a reset, wait past your stated expiry window, and confirm the old link is dead. The OWASP Forgot Password Cheat Sheet is the reference for getting tokens right.

  4. The new password “works,” but you still can’t log in. The set-password page shows success, but the new password is rejected at login — or silently never saved. Why it happens: the update wrote to the wrong place, the old hash path is still being checked, or client-side validation accepted a password the server’s rules then rejected without a clear message. Verify it: finish the reset, then immediately log in with the new password (it must work) and try the old one (it must be rejected). This is the step a quick self-test most often skips — and the one that actually matters to the locked-out user.

  5. User enumeration: known and unknown emails get different answers. Type a registered email into “forgot password” and you see “check your inbox”; type an unregistered one and you see “no account found.” That difference lets anyone discover which emails have accounts. Why it happens: the happy path was written to be helpful, and “be vague to strangers” is a non-obvious security requirement. Verify it: submit a known and an unknown address and confirm the response is identical for both. (Again, OWASP has the pattern.)

  6. The diagnostic one: the flow hits a CAPTCHA wall. If your reset request is behind reCAPTCHA or hCaptcha, an automated walk can’t pass it — and the right behavior is to report BLOCKED, not to fake a pass. That’s not “your reset is broken”; it’s “an external test can’t go further without solving a human challenge.” The manual reset you run yourself is the workaround.

How Prufa walks a password reset

Prufa runs the same pattern here it runs everywhere: an LLM does the navigation, plain code does the verification. The LLM reads the page like a user — finds the “forgot password” link, enters the account email, submits — and plain code, against captured browser evidence, verifies what actually happened: the request returned a 2xx and a “check your inbox” state rendered. The LLM never grades the result, so its blind spots can’t become the verifier’s blind spots. (The full walkthrough is in How Prufa verifies a signup flow; a reset is the same machinery pointed at a longer flow.)

What makes reset different from a contact form — where an external check genuinely can’t see your inbox — is that Prufa’s autonomous path has the email loop built in. On a domain you’ve authorized, it provisions one disposable identity per site (a throwaway inbox plus a generated password, encrypted at rest, never a real user’s credentials), then runs the loop as reviewable steps: wait for the reset email to land in that inbox, follow the one-time link with its token preserved intact through the email’s HTML (so token= survives the trip), set a new password, and assert that login works with it. If the email never arrives, the step times out and is recorded as a failure — never a silent hang. That’s the one flow where “we received the email and finished the loop” is something an external tool can actually prove, because the inbox is one it owns.

What the free 60-second audit does for password reset

The free audit on prufa.dev opens your entry URL on a real browser, finds the reset request, submits it, and verifies the request half succeeded — the form posted and a “check your inbox” state rendered — plus it runs the deterministic checks on the page’s markup. No signup, no card, public pages only — run the free reset-flow audit on your entry URL before you ship and you’ll see the request half verified in about a minute.

Completing the whole loop automatically — receiving the email, following the link, setting a new password, logging in — is the credential-backed path: Pro ($99/mo) runs credential-backed and money-flow journeys against an authorized domain with the disposable-identity machinery above. Starter ($29/mo) re-verifies your core flows — signup, login, checkout — daily and on every deploy and alerts you the moment one breaks, which matters because a reset flow that works today can stop sending the day your host changes its mail policy or a DNS record expires, exactly the kind of silent rot that hits an app after launch.

For the rest of the pre-launch surface — sitemaps, meta tags, broken links, console errors — see Website QA checklist before launch. Password reset is one of the flows that list points at and static checkers don’t walk.

What the audit doesn’t do (honest limits)

A functional check proves the reset completes: the email arrives, the link works, the new password logs you in. It does not replace a security review of the token itself — its entropy and predictability, whether it’s truly single-use, how long it stays valid, and whether the flow leaks which emails have accounts. Those are an OWASP-style password-reset security review’s job — pair the functional check with one, the same way you’d pair a flow walk with a secret scanner for exposed keys. The anonymous free audit also can’t read your inbox and can’t pass a CAPTCHA. An external walk verifies the reset works; a security tool verifies the token is safe.

What to do before you launch

In order of impact:

  1. Run the whole loop yourself, once, end to end. Request a reset for a real test account, receive the email, follow the link, set a new password, and log in with it. If you only test one thing, test this — it exercises every step that breaks.
  2. Check the email lands — inbox and spam. In spam → fix your SPF, DKIM, and DMARC records. Nowhere at all → your send path is broken (host mail disabled, wrong credentials, or still logging the link to a console).
  3. Abuse the token. Reset once, then reuse the same link — it must be refused. Let a link sit past your expiry window, then click it — it must be dead.
  4. Confirm the new password actually took. Log in with the new password (it must work) and the old one (it must be rejected). Don’t assume the success screen means the database was written.
  5. Submit a known and an unknown email. The response must be identical, so the form can’t be used to discover who has an account.

Get those five right and you’ve closed the failure modes that turn “Forgot password?” — the one lifeline a locked-out user has — into a dead end on launch day. That five-minute pass is the heart of pre-launch QA for indie hackers — the flows a solo shipper can’t afford to leave untested.

Frequently asked questions

How do I test my password reset flow before launch?

Test the whole loop, not just the first screen. Request a reset for a real test account, then confirm four things in order: the reset email actually arrives (check spam too), the link in it opens the right page and isn't expired, you can set a new password, and you can log in with that new password while the old one is rejected. The request form looking fine tells you almost nothing — reset flows break in the email and token steps, which only the full loop exercises.

Why is my password reset not working in production when it worked locally?

Almost always the reset email or the link. Locally you may have logged the reset URL to the console instead of sending mail, so the send path was never real. In production the email never leaves the server (host mail disabled, missing SPF/DKIM/DMARC, throttled sends) or the link points at localhost or a wrong base URL baked in at build time. The form submits and says 'check your email' either way — the failure is silent. Send yourself a real reset and trace where it stops.

Can an automated tool test a password reset flow?

Yes, more than for a contact form. An external tool can walk the request-a-reset half on a real browser and confirm it submits and shows a 'check your inbox' state. With an authorized test identity on a disposable inbox, it can also complete the loop — receive the email, follow the one-time link with its token intact, set a new password, and verify login works. What it can't do is the deep security review of the token itself (entropy, single-use, expiry); that's a security scanner's job.

What should a password reset flow do when it works correctly?

Five things: send a reset email within seconds of the request, give the same response whether the email is registered or not (so it can't be used to discover accounts), carry a token that is single-use and expires within an hour or so, let the user set a new password that passes your rules, and then accept that new password at login while rejecting the old one. If any step is missing — no email, a reusable link, a new password that doesn't actually take — a locked-out user stays locked out.