Time zones are a nightmare for engineers
Tags: opensource, billing
Published: 2023-03-14 14:04:25

Time zones suck. There’s a litany of posts advocating for the [“end of Time zones”](https://news.ycombinator.com/item?id=12887007). 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](https://twitter.com/peer_rich/status/1621824222417788928) at Cal.com, and the meme by [Daniel](https://twitter.com/daniel_nguyenx) that is just on point.

![Meme on Timezone and building an app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/65ffhaha6zffs1wisb3p.jpeg)

So here is how we approached time zones at [Lago](http://www.getlago.com) (we build open-source metering and usage-based billing) and why you might need to think about time zones for your own product too, if you’re addressing a global customer base.

---


## Billing and Time Zones: why it matters

Yes. 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”. 

### Expiry dates

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. 

### Double counting or missing consumption when the time zone changes

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`.

---


## Our learnings along the way

[Vincent](https://www.getlago.com/blog/the-4-biggest-problems-with-homemade-billing-systems) in our team was the lead on this feature, here are his own words: 

**1. What solutions have you considered for "translating" UTC into the end customer's timezone?**

In general, it is considered best practice to store dates in a unified and comparable format from one record to another. That's why 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.

**2. How did you learn or find best practices about this topic?**

To be honest, I’ve scrolled the web to learn as many things as I could, I think Stackoverflow was the best source of info, for instance:
- https://stackoverflow.com/questions/33535929/rails-postgres-and-timezone
- https://stackoverflow.com/tags/timezone/info

**3. Did you iterate on the approach? What were your key learnings?**

This is not the first time I have had to deal with this challenge, so I relied heavily on my personal experience and the path was "fairly clear" for me. However, we iterated on the "edge cases" which are numerous with this kind of problem, and making things understandable for the end-user is a constant challenge.

If I had to summarize our approach:

- To manage time zones, we should not handle data in 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](https://www.postgresql.org/docs/15/functions-datetime.html#FUNCTIONS-DATETIME-ZONECONVERT) 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](https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html)).

- 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.

```bash
LAGO_URL="<https://api.getlago.com>"
API_KEY="__YOUR_API_KEY__"

curl --location --request PUT "$LAGO_URL/api/v1/organizations" \\
  --header "Authorization: Bearer $API_KEY" \\
  --header 'Content-Type: application/json' \\
  --data-raw '{
    "organization": {
      "name": "Name1",
      "webhook_url": "<https://test-example.example>",
      "country": "CZ",
      "address_line1": "address1",
      "address_line2": null,
      "state": "state1",
      "zipcode": "10000",
      "email": "org@email.com",
      "city": "city125",
      "legal_name": null,
      "legal_number": null,
      "timezone": "Europe/Paris", //Changes billing record to your organization timezone
      "billing_configuration": {
        "invoice_footer": "footer custom",
        "invoice_grace_period": 3,
        "vat_rate": 15.0
      }
    }
  }'
```

In addition to this, you can decide to overwrite this value per customer. The entire billing engine will then follow the exact time zone sets at a customer level. Usage ingestion, expiry dates and invoicing will be triggered based on the timezone of your customer.

```bash
LAGO_URL="<https://api.getlago.com>"
API_KEY="__YOUR_API_KEY__"

curl --location --request POST "$LAGO_URL/api/v1/customers" \\
  --header "Authorization: Bearer $API_KEY" \\
  --header 'Content-Type: application/json' \\
  --data-raw '{
    "customer": {
      "external_id": "5eb02857-a71e-4ea2-bcf9-57d3a41bc6ba",
      "address_line1": "5230 Penfield Ave",
      "address_line2": null,
      "city": "Woodland Hills",
      "timezone": "America/Los_Angeles", //Overwrite the value per customer
      "url": "<http://hooli.com>",
      "zipcode": "91364",
      "billing_configuration": {
        "payment_provider": "stripe",
        "provider_customer_id": "cus_12345",
      }
    }
  }'
```

This translates the entire billing logic to follow your customer’s timezone.

If this seems very simple within Lago, it’s on purpose: we worked hard to abstract the time zone logic, so that you don’t even have to think about it!

Et voilà !