Short answer: scheduled DNS verification should recover abandoned onboarding, while a customer-triggered recheck should provide fast feedback after a DNS change. For an e-commerce customer connecting a custom domain, put both triggers through one idempotent verification operation and alert only when an explicit verification deadline has passed.
The page usually fires after the useful signal was missed. Support sees a merchant who says the TXT record is in place, the onboarding UI still says unverified, and an on-call alert only reports a delayed job. That is not a choice between a timer and a button. It is an external-consistency boundary without a clear state model. DNS answers can be cached, customer setup happens outside the application, and retries can arrive in any order.
Start with the alert, then work backward
A useful page says that a domain crossed its onboarding deadline without a matching challenge record and includes the last observation. A page that only says verification failed makes the responder reconstruct the customer journey from logs. The missing signal is earlier: issue time, last DNS observation, next automatic check, verification generation, and deadline should all be durable state.
Keep customer-owned and platform-owned zones separate in the product decision. A customer-owned zone preserves the merchant's DNS authority, which is often necessary when the merchant already controls web and mail records. A platform-owned zone can remove the setup handoff, but it is unsuitable when the product must not administer the customer's domain. The verification worker needs the same discipline either way.
Short pages win.
The alert-to-action trace should be boring and specific: the responder reads the domain identifier and generation, sees whether the expected TXT value was ever observed, checks whether a customer requested a recheck, and follows the queue attempt that owns that generation. Do not include the challenge value in the alert. It is an authentication artifact, and broad operational delivery does not improve diagnosis.
Should customer-triggered domain verification rechecks replace scheduled polling in onboarding?
No. Scheduled polling catches customers who finish their DNS change and close the browser. A customer-triggered recheck cuts the wait immediately after the record is added. Removing polling strands customers who never return; removing rechecks makes a completed action wait on an arbitrary clock. Both paths must schedule the same work.
The trade-off is false-positive pressure. An aggressive interval can repeatedly query a resolver while its answer is still cached, creating noisy transitions and needless operational load. An overly relaxed interval makes a working setup look ignored. Set a deadline that matches the onboarding promise, use a bounded retry policy beneath it, and show the last check time in the UI. The 15-minute delay in the example is a policy choice, not a claim about universal DNS propagation.
| Signal | Why it exists | Action |
|---|---|---|
| Initial check | Validate the instruction | Store the observed TXT result |
| Customer recheck | Provide immediate feedback | Enqueue the current generation |
| Scheduled check | Recover inactive sessions | Claim due, nonterminal domains |
| Deadline alert | Surface an unmet promise | Page with state and timestamps |
Consider one domain across a single morning. At 09:00 the application issues a challenge and records generation 1. The initial lookup cannot find the expected TXT value, so the domain receives a next-check time of 09:15. At 09:07 the customer adds the record and presses recheck. The button does not run a private verification path; it enqueues generation 1 through the same queue used by the scheduler. The worker observes DNS, persists its result, and leaves the scheduled attempt intact if the expected value is still absent. At 09:15, an atomic claim either gives the scheduler ownership of a fresh attempt or shows that the manual attempt already completed. If the manual attempt matched at 09:14, the scheduled worker sees a terminal record and stops. If neither path matches before the deadline, the alert carries the last observation and the age of the workflow. This repetition in the data model is intentional. It distinguishes a missing record from a delayed observation, and it prevents a browser click from becoming an untracked exception to the worker contract. Don't retry by recursively calling an HTTP handler. Persist the result before another actor can choose the next attempt.
Make it dull.
Put both triggers behind one idempotent operation
The stable key is the domain identifier plus a verification generation. A new challenge increments the generation, so an older queued attempt cannot verify a newer workflow with an obsolete result. The process-local lock below makes the concurrency rule runnable; a multi-instance service needs a database transaction, queue uniqueness constraint, or distributed lock that uses the same key.
package main
import (
"context"
"fmt"
"net"
"sync"
"time"
)
type Domain struct {
ID, RecordName, ExpectedValue string
Generation int
Verified bool
NextCheck, Deadline time.Time
LastObserved string
}
type Verifier struct {
mu sync.Mutex
running map[string]bool
lookup func(context.Context, string) ([]string, error)
}
func (v *Verifier) Verify(ctx context.Context, d *Domain, now time.Time) error {
key := fmt.Sprintf("%s:%d", d.ID, d.Generation)
v.mu.Lock()
if d.Verified || v.running[key] {
v.mu.Unlock()
return nil
}
v.running[key] = true
v.mu.Unlock()
defer func() {
v.mu.Lock()
delete(v.running, key)
v.mu.Unlock()
}()
values, err := v.lookup(ctx, d.RecordName)
if err != nil {
d.LastObserved = "lookup unavailable"
d.NextCheck = now.Add(15 * time.Minute)
return err
}
for _, value := range values {
if value == d.ExpectedValue {
d.Verified = true
d.NextCheck = time.Time{}
d.LastObserved = "matching TXT value observed"
return nil
}
}
d.LastObserved = fmt.Sprintf("%d TXT value(s) observed", len(values))
d.NextCheck = now.Add(15 * time.Minute)
return nil
}
func main() {
v := Verifier{
running: map[string]bool{},
lookup: func(ctx context.Context, name string) ([]string, error) {
return net.DefaultResolver.LookupTXT(ctx, name)
},
}
d := &Domain{ID: "merchant-42", RecordName: "_verify.shop.example", ExpectedValue: "storefront-token", Generation: 1}
if err := v.Verify(context.Background(), d, time.Now()); err != nil {
fmt.Println(err)
}
}
TXT records are a common DNS challenge mechanism. RFC 7489, for example, specifies the DMARC policy record as a DNS TXT record. A verifier should compare the observed TXT values with the specific expected token; a browser redirect, mailbox address, or nameserver name is not interchangeable evidence when the onboarding contract calls for a DNS challenge.
Make the scheduler a recovery path
The scheduler selects due work. It should not contain a second verifier. This small function separates terminal state, deadline handling, and normal due work so the job runner has one predictable input.
func Due(domains []*Domain, now time.Time) (due []*Domain, overdue []*Domain) {
for _, d := range domains {
if d.Verified {
continue
}
if !d.Deadline.IsZero() && now.After(d.Deadline) {
overdue = append(overdue, d)
continue
}
if !d.NextCheck.After(now) {
due = append(due, d)
}
}
return due, overdue
}
In production, claiming a due record must be atomic. Otherwise two workers can observe the same due domain and both run a lookup. Also persist a generation check with the final update: a stale job may finish after the customer has restarted verification, and it must not mark the new generation as verified.
Instrument challenge issued, recheck requested, lookup completed, and matching value observed. The gaps explain different failures. A long issued-to-recheck gap points to setup friction. Repeated rechecks with the same missing result point to record instructions. A matching result with a later deadline alert points to a state-transition or queue investigation.
Page on the broken promise
Page when the deadline passes without a successful observation, not for every retry. If the threshold is too tight, the team gets alerts for ordinary cache behavior and the page loses meaning. If it is too loose, customers remain in incomplete onboarding before anyone sees the pattern. Review the issued-to-verified distribution, manual recheck volume, and expired-domain count before changing the policy. There is no universal interval.
The conclusion is operational, not promotional: customer-owned domains need a durable challenge, one idempotent verifier, a customer recheck for feedback, scheduled polling for recovery, and an alert tied to a declared deadline.
Top comments (0)