# Recipient timezone respected for send time

- **Pattern:** `ab-000606` (`campaign-orchestration-sequencing.cadence-spacing.timezone-aware-sending`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000606
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000606)

## Why it matters

Scheduling all sends in server UTC means a "send at 9am" step reaches a contact in Tokyo at 9pm and a contact in London at 8am in summer, 9am in winter. Open rates for email campaigns vary by as much as 30% based on local time of delivery — sending into the wrong timezone is a measurable conversion loss, not a cosmetic issue. iso-25010:2011 functional-suitability.functional-correctness treats this as a functional accuracy defect: the system executes the correct action (sends the email) at the wrong time (server timezone rather than recipient timezone), producing a systematically degraded outcome for any globally distributed contact list.

## Severity rationale

Medium because server-timezone scheduling consistently delivers emails at suboptimal local times for non-local contacts, producing measurable open-rate and conversion losses.

## Remediation

Store a `timezone` field on contact records (IANA format, e.g. `'America/New_York'`) and use it to convert the step's preferred local send hour to a UTC timestamp before scheduling.

```ts
import { zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz'

function computeSendTime(delayDays: number, preferredHour: number, contactTimezone: string): Date {
  const now = new Date()
  const nowInContactTz = utcToZonedTime(now, contactTimezone)

  const targetInContactTz = new Date(nowInContactTz)
  targetInContactTz.setDate(targetInContactTz.getDate() + delayDays)
  targetInContactTz.setHours(preferredHour, 0, 0, 0)

  return zonedTimeToUtc(targetInContactTz, contactTimezone)
}
```

Fall back to a sensible default timezone (e.g. `'America/New_York'`) when a contact has no timezone set — do not silently use server UTC. Libraries like `date-fns-tz`, Luxon, or dayjs with the timezone plugin all handle DST transitions correctly.

## Detection

- **ID:** `timezone-aware-sending`
- **Severity:** `medium`
- **What to look for:** Check whether send times account for the recipient's local timezone. When a sequence step is scheduled with a delay of "3 days at 9am," verify that "9am" means 9am in the contact's timezone, not the server's timezone. Look for: a `timezone` field on contact records, timezone-aware date calculation using libraries like date-fns-tz, Luxon, or dayjs with timezone plugin, or a reference to the contact's timezone when computing the scheduled send timestamp.
- **Pass criteria:** Send times are computed in the contact's local timezone. A contact in Tokyo receives a "9am" step email at 9am Tokyo time, not 9am UTC. Count all timezone-related fields on the contact model and verify at least 1 is used in scheduling logic.
- **Fail criteria:** All sends are scheduled in UTC or the server's timezone, ignoring recipient location. Contacts in different time zones all receive emails at the same absolute time.
- **Skip (N/A) when:** The audience is entirely in one timezone, or the system does not support per-contact send time preferences.
- **Detail on fail:** `"Scheduled send time computed in UTC — contacts in all timezones receive emails at the same absolute time"` or `"No timezone field on contact records — timezone-aware scheduling not possible"`
- **Remediation:** Compute scheduled times using the contact's timezone:

  ```ts
  import { zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz'

  function computeSendTime(delayDays: number, preferredHour: number, contactTimezone: string): Date {
    const now = new Date()
    const nowInContactTz = utcToZonedTime(now, contactTimezone)

    // Set to preferred hour on the target day in contact's timezone
    const targetInContactTz = new Date(nowInContactTz)
    targetInContactTz.setDate(targetInContactTz.getDate() + delayDays)
    targetInContactTz.setHours(preferredHour, 0, 0, 0)

    // Convert back to UTC for storage and scheduling
    return zonedTimeToUtc(targetInContactTz, contactTimezone)
  }
  ```

## External references

- iso-25010 functional-suitability.functional-correctness

Taxons: data-integrity

HTML version: https://auditbuffet.com/patterns/ab-000606
