Timezones: store UTC, decide in theirs
Store instants in UTC. Store the person's IANA timezone as a separate field. Convert at the two edges only: what a human reads, and whether it is a decent hour to send them something.
The recurring schedule bug
"Every weekday at 09:00" is not an instant. Stored as one, it is correct until the clocks change and then it is 08:00 or 10:00 forever. Store the rule - local time plus a zone - and compute the next instant each time.
# Wrong: an instant, computed once.
next_run = datetime(2026, 9, 12, 6, 0, tzinfo=timezone.utc)
# Right: the rule, resolved each time in the zone it belongs to.
zone = ZoneInfo(account.timezone) # "Africa/Kampala"
local = datetime.now(zone).replace(hour=9, minute=0, second=0)
next_run = (local + timedelta(days=1)).astimezone(timezone.utc)Countries that do not observe daylight saving are not exempt from this bug, because your server's zone might. A cron running in a datacentre that changes clocks will move a job an hour twice a year regardless of where the customer is.
Quiet hours are the one that matters
Order events fire whenever the warehouse system runs. Without a quiet-hours rule in the recipient's zone, somebody's phone lights up at 03:00 with a notification about a parcel. It is the fastest way to convert a useful channel into a blocked number.
- Define quiet hours per account, in local time.
- Hold anything non-urgent until the window opens.
- Decide explicitly what is urgent enough to override - a security code usually is, a delivery update is not.
- Never let a backlog release all at once when the window opens. Four hundred messages at 08:00 is its own incident.
Where the zone comes from
- Ask the account for their business timezone. This is the one to trust for quiet hours.
- Infer from the phone number's country as a fallback for recipients, knowing it is often wrong for people who travel.
- Never guess from the server. The server's zone is a deployment detail and has no relationship to anybody.
Display, and the small kindness
Show times in the reader's zone, and say which zone when it could be ambiguous. "Delivered 14:32" is fine in a conversation; "Scheduled for 09:00 EAT" is what a person setting a campaign needs, because they are about to message people in another country.
Questions people actually ask
Should I store timestamps in UTC?
Yes, always, with an offset-aware type. Store the user's timezone separately as an IANA name, and convert only when displaying or when deciding whether it is a reasonable hour to message somebody.
Why did my scheduled message send an hour early?
Almost always daylight saving. A time stored as a UTC instant for 'every day at 9am' drifts by an hour when the clocks change in the recipient's country.
Njiwa
One inbox for WhatsApp, Facebook and Instagram, and an API that sends over either WhatsApp rail. 7 days free, no card.