Stripe just launched a major update to their support for Usage Based billing. It's a big improvement and has made Prefab's billing code a lot simpler. Let's look at the change and how that impacts the code you need to write.
Usage Based Billing with Stripe
- We have APIs that get used.
- We have products and prices setup in Stripe.
- Now we just have to send usage to Stripe and tally the billing.
How hard could that be? Well, it used to be harder than you'd think, but Stripe's new usage based billing helps a ton.
The new Stripe usage code helps us in 2 main ways:
- Simplifying our code so less of our code needs to understand the customer's Stripe subscription
- More flexibility & reliability around getting the right usage on the right invoice.
Previously...
In the legacy Usage Based Billing, the data model looked like this.
To track customer usage you had to specify the SubscriptionItem
ID like this:
Stripe::SubscriptionItem.create_usage_record(
'{{SUBSCRIPTION_ITEM_ID}}',
{
quantity: 100,
timestamp: 1714398365,
action: 'increment',
}
)
This was a problem for a few reasons. How do we get our usage code the correct subscription item? The source of truth for a customer's subscription has to be Stripe, but if our code needs to understand the details, that means catching a lot of webhooks and trying to maintain a local picture of the subscription. Subscriptions can be complicated beast too. Free trials, upgrades, downgrades & cancellations. SubscriptionItems are tightly tied to a specific Product & Price. There's a lot of opportunity for race conditions or missed edge cases.
The second issue was that this usage record needed to be created during the current billing period. That means that if you want something on the bill for March, you need to write the usage record during March. Again, the edge cases abound. What about usage in the last minute of the hour in March? What about failure scenarios in your billing or aggregation code?
The Update
With the new system, Stripe has added a entirely new concept called a Meter
.
This seems like a small change, but it makes a world of difference.
How Does This Help?
Usage Code needed to understand Subscriptions
Prefab charges based on the number of requests to the Feature Flag evaluation endpoint. But I couldn't just tell Stripe that customer A did 1M API requests. I had to tell Stripe to put 1M API requests onto the specific pro tier pricing subscription item for a given customer.
So now it's all better. To put the usage onto the meter, all we need to know is the stripe_customer_id
.
def create_billable_requests_meter_events(amount, api_usage, event_type)
attr = {
event_name: "billable_requests",
timestamp: api_usage.day.to_time(:utc).to_i,
payload: {
stripe_customer_id: @team.stripe_customer_id,
value: amount,
}
Stripe::Billing::MeterEvent.create(attr)
end
Now we create as many Prices
as we need for the given Meter
. At Prefab that means a Basic, Pro and Enterprise price. I went through a full break down of how we've modeled our pricing and usage in modeling usage based billing at Prefab. Our usage aggregation code is blissfully unaware of trial end dates, upgrades or cancellations.
The price itself can hold the more complex logic. On our Basic tier, there's no up-front cost for 1.5M requests (~= 5k feature flags MAU). After that it's $10 per 1.5M requests. So our price is a Usage Based
, Per tier
, Graduated pricing
. The first tier is 1->15M for $0 and after that it's .00000666 / request.
Very Difficult to align Billing periods
At Prefab, we see a lot of Feature Flag evaluations. Far far too many to be writing the usage to Stripe for each evaluation. We aggregate the data a two different levels, once inside our backend code, with an in-memory aggregation. This outputs to BigQuery. We then do a second level of aggregation to rollup to hourly level data. We store this in Postgres where it's move convenient to query. Finally we have another job that pull from this table and writes the data to Stripe.
Ideally, usage that happens in March should land on the March bill. But it turns out that's easier said than done. For usage that happens in the last hour of the last day of March, just what exactly are the mechanics for getting that usage onto the March bill?
In the legacy system, all billing usage needed to be updated during the current billing period. See the note in the legacy documentation:
Reporting usage outside of the current billing period results in an error.
During the first few minutes of each billing period, you can report usage that occurred within the last few minutes of the previous period. If the invoice for the previous period isn’t finalized, we add that usage to it. Otherwise, we bill that usage in the current period. After the grace period, you can’t report usage from the previous billing period.
Don’t rely on the grace period for reporting usage outside of a billing period. It’s intended only to account for possible clock drift, and we don’t guarantee it.
There's a bunch of built-in delay in this pipeline while we wait for these aggregation steps so it was very possible for things to run past the end of the "clock drift". We could have adopted a more streaming / dataflow version of this, but that wouldn't really solve the problem. Pipelines can freeze / pause too. We want a billing system that can reliably put the right usage on the right bill even if there's more than 5 minutes of latency in the pipes.
The new system supports:
- Recording usage before a Customer has created a subscription.
- A full 1-hour grace period for usage recorded after invoice creation.
- Cancelling events that have been sent to Stripe within the last 24 hours.
- Backdated subscription creation to capture usage from before a subscription was created.
- Mid-cycle updates to prices.
Work Still In Progress
Our billing code is much improved, but we still have some work in progress.
Bucket Pricing
If you charge $100 per 15M requests with the first 15M requests free and the customer makes 16M requests, what should the bill be?
- If you answered $6.66 that suggested you want it to be graduated.
- If you answered $100 that suggests that you're thinking like enterprise software ;)
As engineers, the rate makes sense to us and usage based billing gives me some comfort with non-round numbers. It turns out that software buyers are not necessarily rational however and a lot of people would prefer a bill that is $99, $99, $99 vs a bill that bounces all over the place like: $71, $92, $53.
We haven't exactly decided which way we'll go on this. But for now the limitation is actually a technical one so we are charging in the graduated manner. We would like the flexibility to be able to charge for a "bucket" of requests. What this requires is that we could set the pricing to have a transform_quantity
option. In our case we would transform_quantity / 15_000_000
to get the number of buckets and then charge $99 per bucket. At the time of this writing this was not an option with tiered usage based billing at Stripe, but I'm assured that it's just around the corner.
Conclusion
Stripe's new Usage Based billing support has been a big upgrade for us. Our code has better separation of concerns. We have a much improved story around consistency and reliability even in the face of delays in our aggregation pipelines. We're really excited to see where Stripe takes this next, particularly when it comes to support for detailed breakdowns of our invoices.