DEV Community

Flexprice
Flexprice

Posted on Originally published at flexprice.io

Why usage-based billing is an engineering problem

A product manager walks over and says the company is going to stop charging per email sent and start charging per conversion. The price also moves from $1 per unit to $1.50. Existing customers keep the old rate for three months at $1.20. Free-trial credits go from 500 to 1000, and one enterprise account gets a 90-day trial instead of the standard 60.

None of that is a pricing decision by the time it reaches a backend team. It's a schema change, a migration, a set of effective-dated rules, and a new set of failure modes in a system that directly produces revenue.

Short answer: why is usage-based billing an engineering problem?

Because usage-based pricing moves the invoice off a static plan record and onto a live event stream. The amount a customer owes becomes a function of data your product emits, which means billing inherits every hard problem in data engineering: ingestion at volume, deduplication, ordering, time alignment, and correctness under retry. A wrong number here is not a dashboard bug. It's an incorrect invoice.

Below is what that actually decomposes into.

Data ingestion: capturing every billable unit

Every unit you plan to charge for has to be emitted by your product and landed somewhere durable. The event is the source of truth for the invoice, so the ingestion path carries the same correctness requirement as the invoice itself.

An event is usually small. This is the shape Flexprice accepts:

{
  "event_name": "model.usage",
  "external_customer_id": "cust_123",
  "properties": {
    "credits": 2,
    "model": "gpt-4",
    "region": "us-east-1"
  },
  "event_id": "evt_abc123",
  "timestamp": "2025-08-22T07:05:49.441Z",
  "source": "api"
}
Enter fullscreen mode Exit fullscreen mode

The hard parts sit around that payload.

Throughput. A product that bills on API calls or tokens can emit billions of events a month. That pushes you toward a streaming layer such as Kafka, Pulsar, or Kinesis, with a columnar store such as ClickHouse or Druid behind it for aggregation.

Normalization. Usage signals arrive from the application, from gateways, from caches, and from third-party APIs. Each source has its own idea of what a unit is. Getting them into one schema without double counting is a transformation problem, not a config problem.

Latency. If customers can see a usage dashboard, batch processing shows up as a support ticket. Near-real-time ingestion means a distributed path that holds up under burst without dropping events.

Integrity. Duplicate delivery and silent loss both corrupt an invoice, in opposite directions. Dedupe on a client-supplied identifier is the usual defence, which is why event_id is worth populating even though it's optional. Flexprice documents the full payload contract and the bulk endpoint in Sending Events, and gives you a per-event view in the Event Debugger for when a number looks wrong.

Metering and aggregation: turning events into billable quantity

Ingestion gives you rows. Metering turns rows into a number per customer per period, and that step is where most of the ambiguity lives.

Dimensionality. You rarely meter one thing. You meter per workspace, per end user, per model, per region, and sometimes all at once on the same event stream. The aggregation you pick (count, sum, average, max, count unique, latest, sum with multiplier, weighted sum) determines what you can bill for later, so it's a modelling decision made early. The aggregation reference covers the available functions.

Retries. If a customer's client retries a failed call, is that one billable event or two? The answer has to be the same every time, which means idempotency has to be decided at the meter, not left to whichever service happens to emit the event.

Time. Billing periods have edges. Customers sit in different time zones, subscriptions start mid-month, and proration has to agree with the meter about where the boundary is. Timezone handling and proration have to be defined once and applied consistently, or the meter and the invoice will disagree at period boundaries.

Lag. If the meter runs behind, a customer can blow past a quota before anything notices. By the time the aggregate catches up, the usage has already happened and someone has to decide who eats it.

Rating: applying the price to the quantity

Rating takes 12,000 API calls and produces a line item. The complexity is in the pricing rules, not the arithmetic.

Tiers, volume breaks, overage rates, minimum commitments, and segment-specific pricing all compose. A customer on an enterprise agreement may have a negotiated rate on one meter and standard pricing on everything else. That means price is a lookup against plan, customer overrides, and effective date, evaluated per line item.

Precision matters more than speed here. A rounding rule applied at the wrong level of aggregation changes totals across every invoice in the run, and it usually surfaces as a customer email rather than an alert.

Entitlements: enforcing what the plan allows

Metering tells you what happened. Entitlements decide what is allowed to happen next, and they run in the request path.

That means a quota check sits between your gateway and your handler, and it has a latency budget. It also means limits need to be soft or hard depending on plan, and changing a limit for one customer should not require a deploy. In Flexprice the limit lives on an entitlement, which is a feature linked to a plan, so it is data rather than code.

Rolling windows are the harder case. A limit of 1M tokens per 5 hours does not line up with a billing period, so it needs its own window tracking, an exhaustion signal, and a rule for what happens to usage past the quota. Entitlement grants handle that shape, firing a webhook on exhaustion and billing the excess as overage.

Overage behaviour is a product decision that engineering has to implement cleanly: notify and keep serving, charge the payment method, or block until the period resets.

Usage visibility: the dashboards customers expect

Usage-based pricing shifts spend risk onto the customer, so they expect to see the meter. That means a live usage view, threshold alerts before a limit is hit, and spend caps that actually stop consumption.

All three are real-time aggregation problems with high-cardinality dimensions. Alerts also need suppression logic, because a customer who gets six notifications about the same threshold stops reading them.

Revenue recognition: the part finance inherits

Variable revenue is harder to close and harder to forecast. Recognition has to follow actual consumption to satisfy ASC 606 or IFRS 15, which requires usage records retained at a granularity auditors can reconcile against invoices. Forecasting gets harder too, because the input is customer behaviour rather than a contract value.

What this adds up to

Usage-based pricing requires a metering and billing pipeline that handles high event volume accurately and with low latency. You need data engineering for ingestion, a deliberate model for metrics and pricing logic, integration with the product to surface usage, and audit paths rigorous enough to catch a bad number before a customer does. Most teams that build this in-house end up maintaining a second product alongside the one they set out to build.

Flexprice is enterprise-grade, open source usage based billing infrastructure for AI and SaaS companies. It can be deployed in your own VPC, on-prem, or on Flexprice's managed cloud. Flexprice offers three deployment options, and all three run the same engine. Because Flexprice is open source and self-hostable, usage and revenue data can stay entirely inside your own infrastructure and never reach a vendor's cloud.

The ingestion path is the same one shown above. Post events, define aggregations against them, attach prices, and the invoice is computed from the stream.

curl --request POST \
  --url https://api.cloud.flexprice.io/v1/events/bulk \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: <your_api_key>' \
  --data '{
    "events": [
      { "event_name": "model.usage", "external_customer_id": "cust_123", "properties": { "credits": 2 } },
      { "event_name": "model.usage", "external_customer_id": "cust_456", "properties": { "credits": 5 } }
    ]
  }'
Enter fullscreen mode Exit fullscreen mode

Up to 1,000 events go in a single bulk request. Both regions take the same payload: api.cloud.flexprice.io for India, us.api.flexprice.io for the US, authenticated with an x-api-key header.

If you want to see how the pieces fit before committing to any of it, the architecture overview is the place to start, and the repo is at github.com/flexprice/flexprice.

Top comments (0)