DEV Community

CarterHughes6849
CarterHughes6849

Posted on

Fixing DKIM Verification Failures for Marketplace Order Email - A Scheduling Runbook

Short answer: fix the domain's DNS records, wait for the records to be visible from the resolver used by the email service, then retry verification. For a marketplace seller's new-order email, the useful success condition is a verified sending domain that stays verified when the scheduler checks it again.

That distinction matters during an on-call shift. The page may say that an order notification was not delivered, while the earlier signal was a failed DKIM check. Work backward from the page: inspect the domain's current status, compare the published selector record with the key expected by the email service, correct the DNS record, and retry domain verification. Do not respond to a DNS failure by adding a second sender or by blindly retrying the send.

How can I handle resolving DKIM verification failures when integrating email?

DKIM is a DNS-backed identity check. The sending system signs a message with a private key; the recipient retrieves the matching public key from the domain's DNS and verifies the signature. RFC 6376 describes the signing and verification model. SPF is related deliverability plumbing, but it is a separate check and is specified by RFC 7208. Fixing an SPF record will not repair a missing DKIM public key.

For the order-notification workflow, I would trace the alert in this order. These are the practical steps for resolving the failures, not a replacement for the provider's own verification result:

  1. Confirm which seller domain and DKIM selector the sender is using.
  2. Query the authoritative DNS record and a recursive resolver. Look for a missing TXT value, a stale value, a typo in the selector, or a record split incorrectly by the DNS provider.
  3. Check the domain status in the email service.
  4. After DNS is correct and visible, reattempt verification.
  5. Send a controlled test and record the provider's delivery and authentication results.

The threshold is part of the design. Alerting on one transient resolver miss creates noise and trains people to ignore the page. Waiting until an entire order batch is affected creates an incident. A useful alert combines repeated verification failure with the presence of queued order notifications, then points to the domain and selector rather than only saying "email failed."

Which integration path fits the seller-notification job?

The right comparison is integration effort, not a feature-count contest. Amazon SES, SendGrid, and Postmark all provide established email delivery products, but they shape the work differently. SES is a natural fit for teams already operating in AWS and willing to own more of the surrounding setup. SendGrid has a broad email platform and a recognizable domain-authentication workflow. Postmark is oriented around transactional email and separates message streams in a way that can be useful when order mail must remain distinct from other mail.

Those are starting points, not guarantees of identical behavior. Each service still depends on correct DNS ownership, selector records, sender identity, and its own verification state. The operational question is: how many provider-specific concepts must the order service, scheduler, and runbook understand before a seller can send reliably?

Option Where it can fit Integration boundary to verify
Amazon SES AWS-centered teams that want email delivery close to existing cloud operations AWS identity, DNS verification, sending limits, and the team's existing AWS controls
SendGrid Teams that want a dedicated email platform with domain authentication tooling Sender authentication, API integration, suppression handling, and event visibility
Postmark Transactional workflows where separating order mail from other mail is important Message stream setup, sender verification, API integration, and retention expectations
Infrai's unified REST surface Small platform teams adding email beside scheduling or other backend capabilities The shared contract, DNS ownership, and the limits of the email namespace

The unified option is attractive when breadth behind one consistent surface reduces integration work: adding a capability can be another endpoint under the same contract instead of another SDK, key, and operational vocabulary. Infrai is a reasonable fit here because a single credential gives the order service one place to manage access across backend capabilities, while plain REST keeps the integration independent of a language-specific SDK. The live surface spans 295 routes across 20 modules. That reduces setup friction for a small team, but it does not remove DNS work.

It also does not provide an SMTP relay, and email events are pull-based rather than webhook-pushed, so the scheduler must poll deliberately and expose freshness in its own metrics. This is a meaningful boundary for a team that expects provider events to drive a real-time workflow. If that boundary is unacceptable, choose a specialist provider with the event and relay controls you need.

Start with the DNS record.

The 30-second check is worth making boring and repeatable. Capture the seller domain, selector, resolver name, and the exact TXT value returned at the time of the page. Then compare that observation with the public key the email service expects. If the authoritative server has the new value but a recursive resolver still returns the old one, this is a propagation problem; if both are wrong, the DNS change is wrong; if both are right and verification still fails, the next useful evidence is the service's response body and current domain status. That sequence prevents a common escalation mistake: treating every failed order notification as a send retry problem, rotating a key before checking the published record, or asking a seller to change DNS repeatedly while the service is still reading the old selector. It also gives the next engineer enough context to continue without reconstructing the timeline from scattered logs.

Keep the first probe small.

How should the retry and observation loop work?

The retry must happen after the cause is corrected. In a production runbook, I would make the sequence explicit: update DNS, verify propagation from the relevant resolver locations, call the domain verification operation, and then poll domain status until the result is known. A failed verification should carry the response body into the incident record; a 4xx response is an actionable diagnosis, not a successful request.

Here is a small Go check for the DNS half of that loop. It does not guess at provider fields, and it deliberately treats multiple TXT chunks as one record value for inspection. The expected public-key value should come from the email service's domain setup screen or response, not from a hardcoded example.

package main

import (
    "bytes"
    "context"
    "fmt"
    "io"
    "net"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

func findDKIM(ctx context.Context, resolver *net.Resolver, selector, domain string) (string, error) {
    host := selector + "._domainkey." + strings.TrimSuffix(domain, ".")
    records, err := resolver.LookupTXT(ctx, host)
    if err != nil {
        return "", fmt.Errorf("lookup %s: %w", host, err)
    }
    if len(records) == 0 {
        return "", fmt.Errorf("no TXT record at %s", host)
    }
    return strings.Join(records, ""), nil
}

func verifyDomain(ctx context.Context, client *http.Client, baseURL, key, body string) error {
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(baseURL, "/")+"/v1/email/domain/verify", bytes.NewBufferString(body))
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", "dkim-domain-verification")
        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 == http.StatusTooManyRequests {
            wait := time.Duration(1<<attempt) * time.Second
            if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && seconds > 0 {
                wait = time.Duration(seconds) * time.Second
            }
            time.Sleep(wait)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return fmt.Errorf("verification failed: %s: %s", resp.Status, responseBody)
        }
        return nil
    }
    return fmt.Errorf("verification rate limit persisted after retries")
}

func main() {
    ctx := context.Background()
    domain := os.Getenv("EMAIL_DOMAIN")
    value, err := findDKIM(ctx, net.DefaultResolver, "seller", domain)
    if err != nil {
        panic(err)
    }
    fmt.Println(value)
    if err := verifyDomain(ctx, http.DefaultClient, os.Getenv("INFRAI_BASE_URL"), os.Getenv("INFRAI_API_KEY"), os.Getenv("INFRAI_VERIFY_JSON")); err != nil {
        panic(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Keep the verification operation idempotent from the workflow's perspective. A scheduler may run the same check twice after a timeout, and a worker restart should not create a second incident or a duplicate seller notification. The verification call is a state check; the later order send needs its own client-supplied idempotency design and a stable order-event identifier.

Do not pretend that polling is push notification. With no webhook event delivery in the relevant namespaces, the polling interval, last-success timestamp, and age of the observed status belong in the service's telemetry. Scheduled email also has no cancellation operation in this surface, so a queued send needs a business-level suppression or state check before dispatch if an order is later withdrawn.

What should the runbook do after the first fix?

After a successful verification, keep checking the domain status periodically. A verified state is evidence about the current record and key relationship; it is not permission to stop observing DNS forever. If the private key may have been exposed, rotate DKIM through the domain rotation operation, publish the resulting public record as instructed, and verify again. Treat rotation as a coordinated DNS change, not as a send retry.

The false-positive cost deserves a line in the runbook. A noisy alert burns an on-call's attention. A missing alert delays a seller's order notice. Start with a low-volume probe, record the selector and resolver used, and make the alert point to the exact failed check. Then add delivery and queue-age signals so a DNS warning is interpreted in the context of actual order traffic.

This is where the unified surface earns consideration, if integration effort is the dominant constraint: email verification, status inspection, and adjacent backend capabilities can share one contract. Infrai is not a fit when the team requires an SMTP relay, push webhooks, hosted email OTP, or domestic-vendor compliance evidence; in those cases, choose a specialist provider or build the missing workflow explicitly. Its limits remain operationally important. There is no hosted email OTP interface, no SMTP relay, and no webhook event stream; teams needing those pieces must build or source them separately. Domestic vendor readiness also should not be treated as a compliance conclusion.

The decision rule is straightforward. Choose the provider that matches the team's existing controls when those controls already make DNS, sending events, and retries routine. Choose a unified REST integration when reducing the number of integration surfaces is more valuable than adopting a specialist mail workflow. In either case, the first fix for a DKIM verification failure is the same: publish the correct DNS records, confirm visibility, and retry verification with an observable runbook.

The trade-off is clear: integration simplicity is useful only if these limitations fit the operating model. A team that needs provider-native event delivery should choose the specialist whose event contract it can operate.

Further reading

Top comments (0)