Luau classify context

When a plan defines classify = function(ctx) ... end, the engine calls it after JSON classify and limits (if configured), and before committing each usage event (POST /usage, StatsD, usage.recorded). Pipeline: Plan extensions → ingest.

When classify(ctx) runs

Only when the user has an active subscription on a plan whose Luau script defines classify. The engine runs classify(ctx), then records usage with the (possibly rerouted) metric. Errors and timeouts reject the usage event (HTTP 400).

Your app can keep sending one metric code (for example tokens or api_requests) — classify decides which plan meter actually receives the event.

Examples

Peak / off-peak meters

An LLM API records a single tokens counter from the SDK. The plan defines separate meters for business hours and nights/weekends, each with its own included allowance. Classify reroutes by ctx.hour (UTC).

time-routed.luau
01 return plan("time_routed", "Time Routed", {
02 price = 1000,
03 metrics = list(
04 metric("tokens", "Tokens", "token"),
05 metric("tokens_peak", "Peak Tokens", "token", { included = 1000 }),
06 metric("tokens_offpeak", "Offpeak Tokens", "token", { included = 1000 }),
07 ),
08 classify = function(ctx)
09 -- SDK always sends "tokens"; billing splits by UTC hour
10 if ctx.metric_code ~= "tokens" then
11 return { metric_code = ctx.metric_code }
12 end
13 if ctx.hour >= 9 and ctx.hour < 18 then
14 return { metric_code = "tokens_peak" }
15 end
16 return { metric_code = "tokens_offpeak" }
17 end,
18 })

Hard cap at ingest

A free plan includes 100 API requests per billing period. Instead of letting usage accumulate and billing overage later, classify rejects the request as soon as month_used + quantity would exceed the cap — your app gets 400 and can show an upgrade prompt immediately.

hard-cap.luau
01 return plan("starter", "Starter", {
02 price = 0,
03 metrics = list(
04 metric("api_requests", "API Requests", "request", { included = 100 }),
05 ),
06 classify = function(ctx)
07 -- reject before the event is stored (HTTP 400)
08 if ctx.month_used + ctx.quantity > 100 then
09 return { allow = false, reason = "monthly API request cap exceeded" }
10 end
11 return {}
12 end,
13 })

Bulk vs standard routing

Batch jobs send the same api_requests metric as interactive traffic, but large payloads (500+ units per event) should bill under a cheaper bulk meter. Classify inspects ctx.quantity on each event.

bulk-routing.luau
01 return plan("api_batch", "API Batch", {
02 price = 5000,
03 metrics = list(
04 metric("api_requests", "API Requests", "request", { included = 10000 }),
05 metric("api_requests_bulk", "Bulk API Requests", "request", { overage = 1 }),
06 ),
07 classify = function(ctx)
08 if ctx.metric_code ~= "api_requests" then
09 return { metric_code = ctx.metric_code }
10 end
11 -- large batches bill under a separate meter with its own tiers
12 if ctx.quantity >= 500 then
13 return { metric_code = "api_requests_bulk" }
14 end
15 return {}
16 end,
17 })

Fields

FieldTypeDescription
user_idstringUser recording usage
metric_codestringSubmitted metric code
quantityintegerUnits in this event
plan_codestringActive plan for this user
period_usedintegerUsage on the current metric (or its group) in the billing period before this event
sourcestringhttp, statsd, or system
month_usedintegerTotal usage across all plan metrics in the current billing period, recorded before this event — useful for caps
occurred_atRFC3339Event timestamp
hourintegerHour of day in UTC (0–23)
weekdayintegerDay of week UTC (0 = Sunday)
hour_localintegerHour in the user's billing timezone (0–23)
weekday_localintegerDay of week in the user's billing timezone
month / day_of_monthintegerUTC calendar month (1–12) and day of month (1–31)
month_local / day_of_month_localintegerSame fields in the user's billing timezone

JSON classify.routes supports the same calendar, quantity, period-usage, and source predicates without Luau — see Plan extensions → JSON classify.

Return value

classify(ctx) must return a table:

  • { metric_code = "..." } — keep or override the target metric (must exist on the plan)
  • { allow = false, reason = "..." } — reject usage
  • {} or omitted metric_code — keep the submitted metric

See Pricing → Limits for timeout and script size constraints.