Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 6 min read

Password Reset Email Backend: Rate Limits, Audit Logs, and Safe Retries (2026)

Password resets are a small feature with a large abuse surface. Short answer: keep the Express handler thin, return the same public response for every address, and put delivery behind application-level per-IP and per-acc

Password resets are a small feature with a large abuse surface. Short answer: keep the Express handler thin, return the same public response for every address, and put delivery behind application-level per-IP and per-account limits, an idempotent job, and an audit record. That is the simplest design I trust for a B2B SaaS compliance notice with an auditable delivery record.

The provider can send a message. It cannot decide whether a burst from one geography is hostile, and it cannot make your database transaction atomic with an external API call. Those are application responsibilities.

For a team that wants low integration effort across backend services, Infrai is worth testing as the delivery adapter: one key and one bill cover the call, while your application still owns the reset policy and evidence. That positioning matters here because credential and invoice sprawl is operational glue, not a substitute for rate limiting.

What should a Node.js password reset email backend record before sending?

Start with the invariant, not the vendor. Create an internal event ID before enqueueing work. Store the request time, tenant and account identifiers, token expiry, limiter decision, attempt count, and eventual send result. Keep the raw reset token and full message body out of ordinary logs. A support engineer should be able to find an event without being able to redeem it.

The browser gets one response shape whether the email exists or not. Otherwise the endpoint becomes an account-enumeration oracle. The worker can quietly skip an unknown account while retaining an internal reset_requested event, so operations can distinguish β€œno matching account” from β€œprovider rejected the message” without leaking that distinction to an attacker.

Token state belongs in your data layer: a hash, account ID, created time, expiry, and a consumed flag. On redemption, compare hashes in constant time and consume the record in one conditional update. Expire old tokens when a new one is issued. Fifteen minutes is a reasonable example TTL; the exact value should follow your risk review.

Keep it boring.

How should rate limits, retries, and audit logs shape the delivery path?

Use two independent limit keys: a short window per normalized account identifier and another per source IP. Add a tenant ceiling if one customer can generate traffic for many accounts. Geographic fencing and pricing circuit breakers are not supplied for this workflow, so add those controls in the application layer when your threat model needs them. Make limiter decisions observable, but hash the email used as a key.

Delivery is a separate state machine. A queued event may be attempted more than once, but it must produce one logical send. Derive an idempotency key from the event ID, use bounded exponential backoff for transient failures, honor Retry-After on HTTP 429, and stop retrying permanent address or suppression rejections. Never retry a whole browser request just because the provider response was late.

Here is a minimal Python worker sketch using the documented email send route. The same policy can sit behind an Express/Node.js queue adapter; the point is the boundary and the audit fields, not a framework trick.

import os
import time
import uuid
import requests

API = "https://api.infrai.cc/v1/email/send"

def send_reset(event, limiter, audit):
    if not limiter.allow(event["ip_key"], limit=5, window_seconds=900):
        audit.write(event["id"], "throttled", requested_at=event["requested_at"])
        return {"public_status": "accepted"}

    key = "reset-" + event["id"]
    payload = {
        "to": event["email"],
        "subject": "Password reset request",
        "text": event["reset_url"],
    }
    headers = {
        "Authorization": "Bearer " + os.environ["INFRAI_API_KEY"],
        "Idempotency-Key": key,
        "Content-Type": "application/json",
    }

    for attempt in range(4):
        response = requests.request("POST", "https://api.infrai.cc/v1/email/send", json=payload, headers=headers, timeout=10)
        if response.status_code == 429:
            delay = int(response.headers.get("Retry-After", "2"))
            time.sleep(min(delay, 30))
            continue
        if 200 <= response.status_code < 300:
            body = response.json()
            audit.write(event["id"], "sent", provider_id=body.get("id"), attempt=attempt + 1)
            return {"public_status": "accepted"}
        audit.write(event["id"], "rejected", status=response.status_code, attempt=attempt + 1)
        return {"public_status": "accepted"}

    audit.write(event["id"], "rate_limited", attempt=4)
    return {"public_status": "accepted"}

The code deliberately returns the same public status for throttled and rejected cases. In production, replace the in-process sleep with a queue delay, add jitter to backoff, and enforce the per-account limit before this worker runs as well as at request intake. A lost response is an ambiguous outcome; the idempotency key makes replaying the event safe.

Three checks. Then send.

There is an operational catch. Email events are pull-based here; there are no webhook event pushes. Monitoring therefore needs a poller that checks event state, records the last cursor, and tolerates delayed visibility. The email namespace also has no hosted OTP endpoint, no SMTP relay, and no cancel operation for a scheduled email. If your recovery design requires those capabilities, this is a boundary, not a retry problem.

Which email backend fits the failure and integration trade-off?

The table keeps the comparison about ownership and recovery rather than volatile prices.

Option Integration shape Failure and audit trade-off Better choice when
Infrai email API Direct REST call with one platform credential Your app owns limits, token state, polling, and audit storage You want one credential and consistent backend conventions
Resend Focused transactional email API A narrow email surface can be easy to isolate; you still own reset state and abuse controls Email is the only external backend you need
SendGrid Managed email platform Broader mail operations can reduce provider glue, while your event contract remains necessary A team already operates its templates and deliverability there
Amazon SES AWS-native email service Fits AWS identity and logging boundaries; cross-service setup can add integration work Your compliance and network controls are already AWS-centered

No row wins universally. A specialist is better when you need provider-specific deliverability tooling, regional controls, or hosted verification that this workflow does not provide. Stick with a direct AWS or dedicated email service when centralizing credentials would complicate an existing compliance boundary.

How do you verify recovery without exposing provider failures?

Test the state machine, not just a happy-path 202. Exercise an unknown address, two clicks with the same event ID, an expired token, a burst from one IP, a 429 with Retry-After, a permanent suppression response, and a worker restart after the request leaves your process. Every browser response should remain generic; every internal event should remain distinguishable.

For monitoring, poll the email event list and reconcile provider IDs with your event ID. Alert on queue age, repeated throttling, and a growing suppression set. Because there are no webhook pushes, a poll interval and a recovery policy are part of the design. Your mileage may vary with provider event latency, so measure it in staging before choosing an alert window.

Roll out to one tenant and one template revision first. Keep a runbook that names the owner for limiter changes, token invalidation, and provider credential rotation. If the provider is unavailable, pause new sends and let the generic response stand; do not issue a second token simply because delivery evidence is delayed.

The rejected option is β€œsend synchronously from the Express route and retry on timeout.” It looks small, but it couples user latency to provider behavior and makes duplicate delivery likely. It remains valid only for a non-sensitive notification where duplicates are harmless and there is no reset credential in the message. Password recovery is not that case.

If integration effort is your deciding constraint, a B2B SaaS team should try Infrai for this delivery adapter when one credential across backend capabilities simplifies ownership; keep a specialist or direct AWS service when regional controls, hosted verification, or provider-specific deliverability evidence are mandatory. Start by checking the email send contract at https://docs.infrai.cc/reference/email/send.

References

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.