-
-
Notifications
You must be signed in to change notification settings - Fork 321
Timezones: survival tips for engineers
Time zones suck. There’s a litany of posts advocating for the “end of Time zones”. There are pros and cons but the net is they won’t go anywhere.
The most fascinating thing about this is that there’s a lot of literature on “how to work across time zones” but so little on “how to build a product that has to handle timezones”.
We’ve come across this tweet from Peer at Cal.com, and the meme by Daniel that is just on point.
So here is how we approached and survived time zones at Lago, and why you might need to think about time zones for your own product too, if you’re addressing a global customer base.
Your timezone may not match your customer's. This could lead to discrepancies in usage ingestion, subscriptions, and invoicing, which may not occur on the date you anticipated.
If your customer doesn’t understand what they’re billed for, it erodes trust and will incur “billing disputes”.
The most telling example is how time zones impact expiry dates.
Expiry dates can be set for contracts, coupons, or wallets that hold prepaid credits to cover future usage.
If you’re a company based in Paris, France, and have a customer in San Francisco, USA, with a coupon expiring on 2023-02-01T00:00UTC
(February 1st, 2023 at midnight). Your customer might expect to be able to use their prepaid credits until the end January 31st, 2023, their time: Pacific Standard Time.
With this day finishing nine hours earlier in the French time zone, that might trigger a “bad surprise” for the user. That kind of discrepancies can also impact subscriptions (when they start or end) and gaps in your customers’ financial records.
Then comes more tedious edge cases that can have considerable impact on billing and the end user’s experience.
Managing the boundaries of a billing period when a customer or an organization changes their timezone is one of them. For example, let’s say a customer had an active subscription, whose timezone is UTC, and for which the billing frequency is “monthly”. In February, we must take into account all the events that can impact their billing (e.g., additional consumption) that occurred between February 1, 2023, at 00:00:00 UTC and February 28, 2023, at 23:59:59 UTC.
If on February 15, this customer changes their time zone to Tokyo time (UTC+9) within our product, the "new billing period" that will be calculated will run from January 31, 2023, at 14:00:00 UTC to February 28, 2023, at 13:59:59 UTC.
The risk is that events received between January 31, 2023, at 14:00:00 UTC and February 1, 2023, at 00:00:00 UTC will be charged twice, once for January UTC and again for February UTC+9.
Therefore, we need to "realign" the start of the period to February 1, 2023, at 00:00:00 UTC.
If, instead of changing to Tokyo time, the customer switches to Los Angeles time (UTC-8), the "new billing period" will run from February 1, 2023, at 08:00:00 UTC to March 1, 2023, at 07:59:59 UTC.
In that case, we need to "realign" the start of the period to January 31, 2023, at 13:59:59 UTC.
We decided to store dates in UTC (in the ISO 8601 format) because it provides a unique reference and is not sensitive to changes in time or modifications to the organization's or customer's timezone. Dates are only converted to the user's format at the last moment for display. We had considered storing dates in the database along with timezone information, but ultimately it made the recording process much more complicated, raised questions about updates, and introduced significant complexity in date comparisons because the organization's and customer's time zones may not be the same.
Btw, Stackoverflow was surprisingly the best source of info, for instance: https://stackoverflow.com/questions/33535929/rails-postgres-and-timezone, or https://stackoverflow.com/tags/timezone/info
To manage time zones, we decided not to hangle the "date" format (without the time information) because a date only makes sense in a particular time zone. This impacts things like usage limits for coupons, or the start and end of a subscription. For certain objects, such as invoices, we need a blocked "accounting date" because it is displayed in a document. We have decided to always define it in the timezone applicable to the customer. Automatic tasks should be processed on an hourly basis, never on a daily basis, because some elements might have “switched” day depending on the time zone, and that impacts billing. This means that instead of wondering "who should I bill today?", the real question is "what are the subscriptions for which we are currently on YYYY-MM-DD?" And safeguards need to be added to avoid billing the same subscription multiple times. Postgres (our database) is very helpful for managing time zones, particularly thanks to the AT TIME ZONE operator.
There are many different time zones, depending on the offset from the GMT reference, daylight saving time, and other fun subtleties (for example, some zones in Asia/Oceania have offsets set on quarter-hours). We use a simplified list of 134 zones (source).
And another fun fact (We had a lot of fun!): Time Zones often have a "friendly name" in the format Continent/City|Island except for UTC and... GMT+12, which only covers two uninhabited American islands :D The result
By default, Lago is ingesting usage and invoicing customers based on the system-wide UTC. However, you can decide to change the value at your organization level to make sure that every single customer is billed based on your timezone.
Et voilà ! How did you handle timezones within your product?