Time zones: engineering headaches I ran into
Time zones are a headache, but they are a necessary evil. I recently built a feature for managing billing in multiple time zones, and here are some things I learned:

1. To manage time zones, we should not handle data in the “date” format because a date only makes sense in a particular time zone. This impacts usage limits for coupons and the start and end of a subscription.
2. Certain objects, like invoices, 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. Instead of wondering “who should I bill now?” the real question is “what are the subscriptions for which we are currently on YYYY-MM-DD?” Safeguards need to be added to avoid billing the same subscription multiple times.
3. Postgres was very helpful for managing time zones, particularly thanks to the AT TIME ZONE operator.
4. There are many different time zones, depending on the offset from the GMT reference, daylight saving time, and other subtleties. We use a simplified list of 134 zones (https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html)
5. Time zones often have a “friendly name” in the format Continent/Location except for UTC and... GMT+12, which only covers two uninhabited islands.

By default, at Lago (https://www.getlago.com), we ingest usage and invoice customers based on the system-wide UTC. However, our users can decide to change the value at their organization level to make sure that their end-users are billed based on their organization’s timezone. In addition to this, they can decide to overwrite this value per end-user. The entire billing engine will then follow the exact time they chose for the end-user. Usage ingestion, expiry dates, and invoicing will be triggered based on the timezone set for their end-user: organization's or customer's one.
My work was to abstract this complexity and make it a feature.

—PS: Why do time zones matter?
Two examples:
1. 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.
2. Managing the boundaries of a billing period when a customer or an organization changes their timezone can have considerable impact on billing and the end user’s experience. 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.

This post aims to provide a quick recap of what I learned while building a timezone feature. If you have any recent learnings or issues, feel free to share them. We’re continuously learning and would love to hear from you.