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 fact | Operation | State change |
|---|---|---|
| Discrete units, such as API calls or tokens | Counter increment | Adds a quantity to the current usage window when allowed. |
| Distinct or active members, such as monthly users or seats | Identity-set touch, assign, unassign, or sync | Adds, refreshes, removes, or reconciles membership. |
| Current or peak state, such as storage in GB | Gauge set | Replaces the current reading or updates the window maximum, according to the meter definition. |
| Customer-facing usage and remaining allowance | Usage summary | Read-only; no usage or decision state changes. |
| Named product or analytics event | Event capture | Records an event; it does not make a pre-action allow or deny decision. |
| Temporary capacity, such as running jobs | Concurrent lease | Acquires 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.