Valqio Docs
Guides

Metering, usage, events, and concurrency

Record usage with the right meter semantics, read customer usage, capture product events, and manage concurrent work.

Valqio provides first-class usage metering as well as managed runtime enforcement. Metering records and summarizes what happened. Enforcement decides whether costly or billable work should run and manages the transaction lifecycle around it.

Use the operation that matches the product fact you need to represent:

Product factOperationState change
Discrete units, such as API calls or tokensCounter incrementAdds a quantity to the current usage window when allowed.
Distinct or active members, such as monthly users or seatsIdentity-set touch, assign, unassign, or syncAdds, refreshes, removes, or reconciles membership.
Current or peak state, such as storage in GBGauge setReplaces the current reading or updates the window maximum, according to the meter definition.
Customer-facing usage and remaining allowanceUsage summaryRead-only; no usage or decision state changes.
Named product or analytics eventEvent captureRecords an event; it does not make a pre-action allow or deny decision.
Temporary capacity, such as running jobsConcurrent leaseAcquires a slot, then releases it when work finishes.

For work that must be blocked before cost is incurred, use runtime.enforce or a reservation instead of recording usage after the fact.

Shared request identity

The examples use a runtime client configured as shown in the Node SDK guide.

const requestId = 'req_123'
const ownerSubject = {
  type: 'customer' as const,
  key: 'workspace_acme',
}

Use a stable idempotency key for each mutation. Reuse it when retrying the same product attempt; create a new key for a new attempt.

Increment a counter

Counters represent additive usage. The response says whether the limit allowed the increment and whether Valqio applied the mutation.

const counted = await runtime.meters.counters.increment({
  meterKey: 'api-calls',
  ownerSubject,
  quantity: 1,
  idempotencyKey: `api-call:${requestId}`,
})

if (!counted.allowed) {
  throw new Error(`API call usage denied: ${counted.reasonCode}`)
}

console.log(counted.current, counted.remaining, counted.mutationApplied)

Use runtime.enforce when the same call must decide and count before product work starts. Use a reservation when work can fail after approval and the held usage must later be committed or released.

Track distinct or active identities

Use touch for identities counted once per window, such as monthly active users. Use assign and unassign for durable or leased membership, such as seats or active domains. Use sync when an external system owns the complete membership list.

const activeUser = await runtime.meters.identity.touch({
  meterKey: 'monthly-active-users',
  ownerSubject,
  actorKey: 'user_42',
  idempotencyKey: `mau:${requestId}:user_42`,
})

if (!activeUser.allowed) {
  throw new Error(`Active-user limit reached: ${activeUser.reasonCode}`)
}

Assign and unassign explicit membership with the same owner and a stable identity key:

await runtime.meters.identity.assign('user_42', {
  meterKey: 'seats',
  ownerSubject,
  idempotencyKey: 'seat:workspace_acme:user_42:assign:v1',
})

await runtime.meters.identity.unassign('user_42', {
  meterKey: 'seats',
  ownerSubject,
  idempotencyKey: 'seat:workspace_acme:user_42:unassign:v1',
})

await runtime.meters.identity.sync({
  meterKey: 'seats',
  ownerSubject,
  idempotencyKey: 'seat:workspace_acme:directory-sync:v12',
  mode: 'replace',
  members: [{ actorKey: 'user_42' }, { actorKey: 'user_84' }],
})

Repeated touches and assignments are safe when they reuse the identity and idempotency key. Use mode: "replace" only when the supplied list is authoritative; use mode: "upsert" to add or refresh members without removing omitted ones. Inspect alreadyCountedThisWindow, alreadyAssigned, memberActive, or leaseState when those fields apply to the configured identity-set mode.

Set a gauge

Gauges represent an absolute reading rather than an increment.

const storage = await runtime.meters.gauges.set({
  meterKey: 'storage-gb',
  ownerSubject,
  value: 128,
  idempotencyKey: `storage:${requestId}:128`,
})

if (!storage.allowed) {
  throw new Error(`Storage limit reached: ${storage.reasonCode}`)
}

The meter definition determines whether the runtime keeps the latest value or the maximum value in a window. Send the full reading, not a delta.

Read a usage summary

Usage summaries are read-only customer truth. They combine current usage, configured limits, remaining allowance, plan context, and snapshot freshness without mutating usage or writing a decision.

const usage = await runtime.usage.summary({
  subject: ownerSubject,
  includePlan: true,
})

const apiCalls = usage.meters['api-calls']
console.log({
  current: apiCalls?.current,
  limit: apiCalls?.limit,
  remaining: apiCalls?.remaining,
  status: apiCalls?.status,
  wouldAllowOne: apiCalls?.wouldAllow?.allowed,
})

Treat warnings and snapshot freshness as part of the response. Do not turn an unknown or stale summary into an allow decision; call the enforcement operation when product work needs a decision.

Capture product events

runtime.capture records named events for evidence and downstream analysis. Give each event a stable eventId so retries preserve its identity.

await runtime.capture({
  eventId: `invoice-sent:${requestId}`,
  eventName: 'invoice.sent',
  capability: 'sendInvoice',
  subject: { type: 'customer', id: ownerSubject.key },
  fields: { invoiceId: 'inv_123' },
})

Event capture does not itself enforce a meter, spend credits, or settle a reservation. Do not call an action and its underlying meter or event operation for the same unit unless the product model intentionally requires separate records.

For higher-volume event ingestion, enable the SDK's bounded buffer when creating the client:

import { createRuntimeClient } from '@valqio/sdk-node'

const bufferedRuntime = createRuntimeClient({
  apiKey: process.env.VALQIO_API_KEY!,
  baseUrl: process.env.VALQIO_DATA_PLANE_URL!,
  projectId: process.env.VALQIO_PROJECT_ID!,
  environment: process.env.VALQIO_ENVIRONMENT!,
  mode: 'remote-only',
  capture: {
    mode: 'buffered',
    batchSize: 100,
    overflow: 'reject',
  },
})

await bufferedRuntime.capture({
  eventId: `invoice-sent:${requestId}`,
  eventName: 'invoice.sent',
  subject: { type: 'customer', id: ownerSubject.key },
})

await bufferedRuntime.flush()
await bufferedRuntime.close()

Call flush() during graceful shutdown and handle failures according to your delivery requirements. close() stops the timer and performs a final best-effort flush.

Limit concurrent work

Use a concurrent lease for temporary capacity such as running jobs or active sessions. The helper acquires before the callback, passes the lease ID to the work, and attempts release in a finally block.

const result = await runtime.concurrent.withLease(
  {
    limitKey: 'running-jobs',
    subject: ownerSubject,
  },
  async (leaseId) => {
    await recordLeaseForSupport({ requestId, leaseId })
    return runJob()
  },
)

The helper throws a rate-limit error when no slot is available. Release is best effort, so configure a lease TTL as the safety net. For durable asynchronous jobs, use runtime.concurrent.acquire and runtime.concurrent.release directly, store the lease ID with the job, and reconcile failed releases.

Inspect the result

  • Open Decisions → History for pre-action enforcement decisions.
  • Open Runtime Activity for usage mutations, reservations, and settlement.
  • Open Events for captured product events.
  • Keep request IDs, idempotency keys, reservation IDs, and lease IDs in non-secret application logs.

Billing providers invoice and collect payment. Valqio meters usage, decides whether product work should run, manages usage transaction state, and records the evidence that explains downstream billing input.

On this page