DocsRecipes

Recipes

Usage metering and quota analytics

4 min readReviewed July 2026

GraphJSON can analyze billable usage and power customer-facing quota dashboards. It should not be the only authoritative ledger that decides invoices, balances, credits, or whether a customer request is allowed.

Use a one-way analytical copy:

product action
  → authoritative usage ledger
      ├─ billing provider
      ├─ entitlement or quota enforcement
      └─ GraphJSON analytical event
             ├─ internal dashboard
             ├─ customer usage portal
             └─ operational alert

This separation lets GraphJSON explain and visualize usage without turning a late analytics event into a billing error.

Define the meter#

For every billable metric, record:

meter name:
billable action:
unit:
aggregation:
customer identifier:
billing period:
deduplication key:
correction policy:
late-arrival policy:
authoritative system:
display freshness:
reconciliation owner:

Examples:

Meter Unit Aggregation
API requests request Sum increments
Data processed byte Sum increments
Active seats seat Latest value or time-weighted policy
Stored objects object-day Daily snapshot or derived ledger
Model tokens token Sum input and output separately

Do not use one generic usage number for unlike units.

Record an immutable usage fact#

{
  "event": "usage_recorded",
  "event_id": "usage_evt_01J...",
  "account_id": "acct_7",
  "meter": "api_requests",
  "quantity": 1,
  "unit": "request",
  "billing_period": "2026-07",
  "source": "api_gateway",
  "source_record_id": "req_01J...",
  "occurred_at": 1785081600,
  "schema_version": 1
}

Use the source occurrence time as the GraphJSON request timestamp.

Requirements:

  • event_id is globally stable across retries
  • account_id matches the billing authority
  • meter and unit come from controlled vocabularies
  • quantity is a JSON number
  • billing_period follows the billing system’s zone and boundary
  • source_record_id supports reconciliation without exposing a secret

Write the authoritative ledger first#

A reliable path:

  1. product action completes
  2. write an immutable usage record in the authoritative database
  3. commit product state and usage record together where possible
  4. enqueue provider and GraphJSON delivery
  5. retry each destination independently

Do not synchronously call GraphJSON and treat its response as the only durable usage record.

The ledger should support:

  • idempotent insert by source record ID
  • deterministic billing-period assignment
  • corrections
  • invoice-period queries
  • customer dispute investigation
  • replay to downstream systems

Deliver to GraphJSON#

Batch analytical copies:

async function sendUsageBatch(records) {
  const response = await fetch(
    "https://api.graphjson.com/api/bulk-log",
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        api_key: process.env.GRAPHJSON_API_KEY,
        collection: "usage_events",
        timestamps: records.map((record) => record.occurred_at),
        jsons: records.map((record) => JSON.stringify(record))
      }),
      signal: AbortSignal.timeout(10_000)
    }
  );

  if (!response.ok) {
    throw new Error(
      `GraphJSON ${response.status}: ${(await response.text()).slice(0, 300)}`
    );
  }
}

The worker acknowledges a ledger-export job only after GraphJSON returns 2xx.

Deduplicate before summing#

WITH unique_usage AS (
  SELECT
    JSONExtractString(json, 'event_id') AS event_id,
    any(JSONExtractString(json, 'account_id')) AS account_id,
    any(JSONExtractString(json, 'meter')) AS meter,
    any(JSONExtractFloat(json, 'quantity')) AS quantity
  FROM usage_events
  WHERE
    JSONExtractString(json, 'event') = 'usage_recorded'
    AND toDateTime(timestamp) >= toDateTime('2026-07-01 00:00:00', 'UTC')
    AND toDateTime(timestamp) < toDateTime('2026-08-01 00:00:00', 'UTC')
  GROUP BY event_id
)
SELECT
  account_id,
  meter,
  sum(quantity) AS usage
FROM unique_usage
GROUP BY account_id, meter
ORDER BY usage DESC
LIMIT 1000

any assumes every retry of one event_id has the same payload. Enforce that in the ledger.

Represent corrections#

Never mutate an old analytical event in place. Emit an explicit correction linked to the original:

{
  "event": "usage_corrected",
  "event_id": "usage_correction_01K...",
  "corrects_event_id": "usage_evt_01J...",
  "account_id": "acct_7",
  "meter": "api_requests",
  "quantity_delta": -1,
  "unit": "request",
  "reason": "duplicate_gateway_record",
  "billing_period": "2026-07",
  "schema_version": 1
}

Keep reasons controlled:

duplicate_gateway_record
customer_credit
source_reconciliation
internal_test

The authoritative ledger decides whether a correction changes an open invoice, creates a credit, or belongs in a later period. GraphJSON displays the decision; it does not make it.

Calculate corrected usage#

WITH base AS (
  SELECT
    JSONExtractString(json, 'account_id') AS account_id,
    JSONExtractString(json, 'meter') AS meter,
    sum(
      if(
        JSONExtractString(json, 'event') = 'usage_recorded',
        JSONExtractFloat(json, 'quantity'),
        JSONExtractFloat(json, 'quantity_delta')
      )
    ) AS quantity
  FROM usage_events
  WHERE
    JSONExtractString(json, 'billing_period') = '2026-07'
    AND JSONExtractString(json, 'event') IN (
      'usage_recorded',
      'usage_corrected'
    )
  GROUP BY account_id, meter
)
SELECT account_id, meter, quantity
FROM base
ORDER BY quantity DESC
LIMIT 1000

For exact billing, deduplicate both base records and corrections by their own event IDs before summing.

Model quota and plan changes#

Record plan or quota snapshots separately:

{
  "event": "usage_limit_changed",
  "event_id": "limit_evt_01J...",
  "account_id": "acct_7",
  "meter": "api_requests",
  "limit": 1000000,
  "unit": "request",
  "effective_at": 1785081600,
  "reason": "plan_upgraded",
  "schema_version": 1
}

Do not attach only the current plan to every historical usage event and assume it was always true. Use event-time state or join to explicit state changes.

Quota enforcement belongs in the application or entitlement service. A GraphJSON dashboard can lag and must not decide whether request number 1,000,001 is accepted.

Build a customer usage dashboard#

Start with:

  1. current billing period
  2. usage total by meter
  3. current limit
  4. percentage consumed
  5. daily trend
  6. last-updated time
  7. explanation of delayed or corrected data

Return only an authorized account:

SELECT
  toUnixTimestamp(
    toStartOfDay(toDateTime(timestamp, 'UTC'))
  ) AS day,
  sum(JSONExtractFloat(json, 'quantity')) AS requests
FROM usage_events
WHERE
  JSONExtractString(json, 'event') = 'usage_recorded'
  AND JSONExtractString(json, 'account_id') = 'acct_7'
  AND JSONExtractString(json, 'meter') = 'api_requests'
  AND JSONExtractString(json, 'billing_period') = '2026-07'
GROUP BY day
ORDER BY day

Your server must replace acct_7 with the ID from the authenticated session. The browser must not choose an arbitrary account filter.

See API usage customer portal for embed isolation.

Alert on thresholds#

Useful internal alerts:

  • customer exceeds 80% of quota
  • ingestion falls materially below ledger volume
  • usage spikes beyond an account’s established range
  • ledger export queue age grows
  • records lack account_id, meter, or unit

A quota alert is advisory. Enforcement still reads the authoritative entitlement state.

GraphJSON’s current alert workflow does not provide repeated escalation or guaranteed paging. Use Create and operate alerts.

Handle late events#

Define:

  • invoice close time
  • accepted late window
  • time zone
  • open versus finalized period behavior
  • correction and credit policy

An event’s occurrence time can belong to a closed period even when delivery happens later. Keep both facts:

{
  "occurred_at": 1785081600,
  "recorded_at": 1785168000,
  "billing_period": "2026-07"
}

Do not silently move late usage into the delivery day for convenience.

Reconcile with the billing provider#

For every account and meter:

authoritative ledger total
  ↔ provider accepted meter total
  ↔ provider invoice quantity
  ↔ GraphJSON deduplicated analytical total

Compare:

  • record count
  • unique event IDs
  • quantity by meter
  • period boundaries
  • corrections
  • late records
  • provider rejections
  • currency and price outside the usage metric

GraphJSON and billing-provider summaries can update at different times. Document the expected delay before treating a temporary difference as a dispute.

Customer disputes#

Keep:

  • meter definition
  • source record IDs
  • occurrence timestamps
  • deduplication evidence
  • correction history
  • invoice-period boundaries
  • version of the price and quota policy

Do not use a screenshot of a GraphJSON chart as the only billing evidence.

Launch checklist#

  • Authoritative ledger exists outside GraphJSON.
  • Meter, unit, aggregation, and period are documented.
  • Stable event IDs make retries idempotent.
  • GraphJSON receives the original occurrence time.
  • Corrections are explicit immutable facts.
  • Quota enforcement does not read an analytical dashboard.
  • Customer embeds derive account ID from the server session.
  • Dashboards show freshness and billing-period boundaries.
  • Ledger, provider, invoice, and GraphJSON totals reconcile.
  • Dispute and late-arrival policies have owners.

Pair this recipe with Revenue and subscription analytics, Metric governance, and Reliable event delivery.

Need a hand?

Tell us what you’re building and we’ll point you in the right direction.

Contact support