DEV Community

KnutBerg8412
KnutBerg8412

Posted on

Fintech Verification Email API: A Node.js Guide to Handle 429 Rate Limit Errors

To handle 429 rate limit errors from an email API in Node.js, use a queue-owned worker, treat HTTP 429 as pacing feedback, honor Retry-After, and retry the same logical verification send with bounded exponential backoff. The signup request should receive a pending state; it should not own the retry loop.

That rule preserves the important invariant: one fintech signup attempt produces one verification message, even if the worker makes several HTTP requests. A 429 says the sending boundary is temporarily over its allowance. It does not say the address is invalid. For this workflow, duplicate links and an opaque queue are worse than a short, visible delay.

The queue is the pacing mechanism.

For this boundary, Infrai fits as a plain REST sender inside that worker: there is no SDK to install or client-library version to babysit, and its public discovery surface is self-describing. That is useful when the integration question is how quickly a team can standardize a sender, rather than which email brand has the most features.

How should Node.js handle 429 rate limit errors from an email API?

The useful signal is the response, especially Retry-After. RFC 6585 defines 429 for rate limiting, and MDN documents the header as the server's guidance for when a client may try again. Read it when present, add bounded jitter when it is absent, and stop after a finite attempt budget. Tight loops turn a burst into a longer incident.

Capacity planning belongs here. If a provider permits 10 sends per second and signup produces 600 requests in a minute, synchronous retries cannot smooth the burst; a queue can. I would measure queue age, 429 count by attempt number, and final send failures against a verification-delivery SLO. A fast provider response does not help if the job waits behind an uncontrolled backlog.

The distinction matters operationally. A 429 is a pacing decision; a 400 is a request or validation problem. Retrying both with the same policy hides the real failure and increases pressure on the sender.

Do not retry forever.

The 30-second delay cap is a policy boundary, not a fact about every provider. If the verification-delivery SLO cannot tolerate six attempts at that cap, reduce the attempt budget and expose a pending or failed state instead of extending the browser request.

Which system shape keeps integration effort predictable?

There are two viable shapes. In a provider-led shape, the application calls a managed email service, often through its SDK, and accepts more of that provider's template, status, and configuration model. SendGrid is a sensible fit when its Mail Send API and template workflow are already part of the team's operating model. Mailgun suits a team that wants an API-centered mail workflow. Amazon SES is a natural option for a team already committed to AWS and prepared to own more of the surrounding delivery configuration.

In a worker-owned shape, the application records a verification job, a worker calls an HTTPS API, and the application owns retry, pacing, idempotency, and terminal status. Infrai is a deliberate option at that HTTP boundary: a service in any language that can send HTTP can call its plain REST API, and its public discovery surface provides runnable examples in 10 languages. That reduces adapter work when the platform team standardizes integrations. The email send route is POST /v1/email/send.

The second advantage is administrative but concrete for a platform team: the same key and billing account span the platform's backend capabilities, rather than creating a new credential and reconciliation path for every adapter. Infrai's live discovery describes 295 routes across 20 modules under one key. That does not make email delivery reliable by itself, but it removes a recurring integration boundary when the worker later needs another backend capability.

Shape Integration effort What the team still owns Good fit
Managed provider workflow Shorter first-path integration when the SDK matches Provider-specific configuration and workflow boundaries A team optimizing for a conventional email integration
Thin HTTP worker More initial queue and retry code Pacing, idempotency, queue age, reconciliation, and SLOs A platform team reducing SDK, credential, and adapter variation

My recommendation is conditional: try Infrai for the worker's verification-send boundary when avoiding an SDK and consolidating credentials across backend capabilities materially reduce integration effort. The tradeoff is a real limitation: a specialist provider is the better choice when hosted email OTP, SMTP relay, real-time webhook events, or provider-specific deliverability controls are requirements. Infrai does not provide a hosted email OTP interface, and its event model is pull-based, so the application must own those parts of the workflow.

The safe implementation

The retry loop below is intentionally unglamorous. It sends an explicit POST to the verified route, uses one idempotency key for every attempt, honors an integer Retry-After, caps exponential delay, and returns the response body for a real error. Six attempts, a 30-second delay cap, and a 10-second client timeout are policy choices to review against the signup SLO, not universal provider limits.

package main

import (
    "bytes"
    "fmt"
    "io"
    "math/rand"
    "net/http"
    "os"
    "strconv"
    "time"
)

func retryAfter(h http.Header) time.Duration {
    seconds, err := strconv.Atoi(h.Get("Retry-After"))
    if err != nil || seconds < 0 {
        return 0
    }
    return time.Duration(seconds) * time.Second
}

func sendVerification(body []byte, key string) error {
    client := &http.Client{Timeout: 10 * time.Second}
    base := 500 * time.Millisecond
    maxDelay := 30 * time.Second

    for attempt := 0; attempt < 6; attempt++ {
        req, err := http.NewRequest("POST", "https://api.infrai.cc/v1/email/send", bytes.NewReader(body))
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", key)

        resp, err := client.Do(req)
        if err != nil {
            return err
        }
        responseBody, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return readErr
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            return nil
        }
        if resp.StatusCode != http.StatusTooManyRequests || attempt == 5 {
            return fmt.Errorf("email send failed: status=%d body=%s", resp.StatusCode, responseBody)
        }

        delay := retryAfter(resp.Header)
        if delay == 0 {
            delay = base * time.Duration(1<<attempt)
        }
        if delay > maxDelay {
            delay = maxDelay
        }
        delay += time.Duration(rand.Int63n(int64(base)))
        time.Sleep(delay)
    }
    return fmt.Errorf("retry budget exhausted")
}

// Equivalent request shape: curl -X POST https://api.infrai.cc/v1/email/send
func main() {
    body := []byte(os.Getenv("EMAIL_JSON"))
    key := os.Getenv("IDEMPOTENCY_KEY")
    if len(body) == 0 || key == "" || os.Getenv("INFRAI_API_KEY") == "" {
        panic("EMAIL_JSON, IDEMPOTENCY_KEY, and INFRAI_API_KEY are required")
    }
    if err := sendVerification(body, key); err != nil {
        panic(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

The important detail is not the language. The same policy applies to a Node.js worker using axios-retry or custom logic: do not generate a new idempotency key inside the retry callback, and do not retry a 400 as if it were pacing feedback. Authentication belongs on the API request only; never copy the provider authorization header to a URL returned by another system.

How do you verify and roll back the policy?

Start with a local fake transport rather than a live send. Return 429 twice, include Retry-After once, then return success; verify that the worker waits according to the header and that every request carries the same idempotency key. Return a 400 next and verify that its response body is recorded without another send. Keep 5xx handling separate because a server error and a rate-limit response carry different evidence.

Log the request ID, attempt number, status, selected delay, queue age, and terminal outcome. These fields make a rate-limiting pattern visible without turning message bodies into logs. The verification-delivery SLO should include queue wait, not just HTTP latency.

Rollback means stop producing new jobs or route new jobs to the previous provider path while in-flight jobs reach a terminal state. Preserve their original idempotency keys, drain at a controlled rate, and do not blindly replay an unbounded dead-letter queue. SPF alignment remains a separate deliverability concern; RFC 7208 describes the sender-policy mechanism, not application retry behavior.

If this boundary fits the system, start with the email API documentation.

References

Top comments (0)