API Reference
Async, flushing & reliability
How the SDK sends signals — buffered by default, with retries, idempotency, and the flush() you must call before a serverless function exits.
This page is about the @clocknext/sdk client. If you post to
/api/v1/usage over raw HTTP, none of
the buffering below applies — each call is its own request, and retries,
batching, and idempotency are yours to implement.
The client is async by default: signals.* buffers the signal in memory and
returns immediately. In a serverless function you must await cnk.flush()
before returning, or signals still in the buffer are lost when the process
freezes.
Async by default
In the default async mode, a signal call buffers and returns right away —
it never blocks your model/response path:
// returns synchronously as { queued: true } — the send happens in the background
cnk.signals.credit({
customerId: "cus_abc123",
model: "gpt-4o",
agentKey: "pro_credit",
tokens: { input: 1200, output: 480 },
});A background flusher drains the buffer when either trigger fires first:
- the buffer reaches
batch.maxSizesignals (default 20), or batch.maxIntervalMselapses (default 2000 ms).
Each drain sends up to batch.maxConcurrency requests at once (default 5).
"Batching" here is about timing, not a bulk endpoint — every signal is still
its own POST /api/v1/usage.
In async mode, server-side rejections (e.g. insufficient allowance, 422) are
not thrown from the call — they surface via the onError hook (or when you
flush()). Use { wait: true } or sync mode below if you need to react inline.
Get the priced result back
To receive the computed usageLog (cost, credits drawn, post-signal balance) —
or to gate on a real-time allowance rejection — await the send. Per call:
const res = await cnk.signals.credit(
{ customerId: "cus_abc123", model: "gpt-4o", agentKey: "pro_credit", tokens: { input: 1200, output: 480 } },
{ wait: true },
);
res.usageLog?.customerCost;
res.usageLog?.balance?.remaining;…or globally, so every call awaits its send:
const cnk = new Clocknext({ apiKey: process.env.CLOCKNEXT_API_KEY!, mode: "sync" });Flush before you exit
Because sends are deferred, drain the buffer at the right boundaries:
// Before a serverless function returns (Lambda, Vercel, Cloud Functions):
await cnk.flush();
// On graceful shutdown of a long-running process — flushes, then refuses new signals:
await cnk.close();
// How many signals are still buffered:
cnk.pending;On Vercel & serverless
A serverless (or edge) function is usually frozen the moment it returns, so signals still in the async buffer never send — and a per-request function rarely reaches the size/interval flush trigger, so without one of the patterns below you can silently lose most of your signals.
Flush after the response so metering never adds latency to your handler:
// Next.js App Router route handler on Vercel:
import { after } from "next/server"; // or: import { waitUntil } from "@vercel/functions";
export async function POST(req: Request) {
const out = await callLLM(/* … */);
cnk.signals.credit({
customerId: "cus_abc123",
model: "gpt-4o",
agentKey: "pro_credit",
tokens: { input, output },
});
after(() => cnk.flush()); // runs after the response is sent, before the instance freezes
return Response.json(out);
}waitUntil(cnk.flush()) (from @vercel/functions) is the framework-agnostic
equivalent. Prefer simplicity over throughput? Run the client in sync mode (or
pass { wait: true }) and await each signal — the send finishes before your
handler returns, at the cost of a little latency.
Retries & idempotency
Reads (GET/DELETE) and metering signals opt into automatic retries; other
non-idempotent writes (customers.create, purchases.create, …) do not, so
a transient blip can never create a duplicate.
- Retried: transient transport failures — network errors, timeouts, and
408/409/429/5xx. Backoff is exponential (baseDelayMs · 2ⁿ, capped atmaxDelayMs) with equal jitter, and a serverRetry-Afterheader is honoured. - Never retried: deterministic
400/401/404/422(validation, auth, not-found, plan/allowance). - Idempotency: every metering signal carries an idempotency key — auto-generated
(UUID v4) per signal and reused across all of that signal's retries, so a
retried send is deduplicated server-side instead of double-counted. Pass
your own
idempotencyKey(a stable id for the logical event) to also dedup across process restarts or at-least-once redelivery.
cnk.signals.credit({
customerId: "cus_abc123",
model: "gpt-4o",
agentKey: "pro_credit",
tokens: { input: 1200, output: 480 },
idempotencyKey: `chat:${requestId}`, // survives restarts; the server dedups repeats
});Durability: the buffer is in-memory
A signal the server has already accepted is durable on the server. But signals still sitting in the async buffer when a process crashes (before send) are lost — there is no on-disk spool. For zero-loss-before-receipt:
- run in
syncmode (or pass{ wait: true }) so the send completes before your code continues, or await cnk.flush()/cnk.close()at your function/shutdown boundaries.
When the buffer is full (batch.maxQueueSize, default 10 000) new signals
are dropped rather than queued without bound — observe both drops and permanent
failures with the hooks:
new Clocknext({
apiKey: process.env.CLOCKNEXT_API_KEY!,
onError: (err, signal) => report(err, signal), // permanent failure (after retries / non-retryable)
onDrop: (signal, reason) => report(reason), // "queue_full" | "send_failed"
onFlush: (count) => metrics.add("flushed", count),
});Configuration
Every knob is optional; a bad value (negative / non-finite) falls back to its default rather than breaking the client.
Prop
Type