Serverless and short-lived jobs
Capture batches in the background. That is what keeps it off your request path, and it is also the one thing that can lose you rows: a process that exits, or a runtime that freezes, takes its queue with it.
Anywhere the platform can freeze or kill you between requests, deliver explicitly.
flush() or shutdown()
Section titled “flush() or shutdown()”They are not interchangeable, and picking the wrong one in a serverless handler has a lasting effect.
flush() | shutdown() | |
|---|---|---|
| Delivers queued rows | Yes | Yes |
| Stops the config poller and background worker | No | Yes |
| Capture still works afterwards | Yes | No |
| Use it when | The process stays alive but may freeze | The process is exiting |
flush() takes a timeout and returns whether the queue actually drained. It
never raises. Python’s default is 3.0 seconds and TypeScript’s is 3000
milliseconds, and both are arguments you can change.
delivered = metergraph.flush() # default timeout 3.0 secondsdelivered = metergraph.flush(10.0) # wait longerconst delivered = await mg.flush(); // default timeout 3000 msconst delivered = await mg.flush(10_000); // wait longerPython registers shutdown() with atexit when init() succeeds, so a script
that runs to completion delivers its rows without you doing anything. Node has
no equivalent hook, so in TypeScript call await mg.shutdown() yourself from
your normal shutdown path.
The two transport modes
Section titled “The two transport modes”TypeScript picks a delivery strategy at startup. Python has one strategy: a daemon background thread.
| Mode | Behavior |
|---|---|
background | A timer flushes on an interval, 5000 ms by default. Used for long-running servers |
buffered | No timer. Every enqueued row schedules a flush immediately, and delivery uses keepalive on bodies up to 64 KiB |
The default is auto, which chooses buffered when it sees any of:
AWS_LAMBDA_FUNCTION_NAMEin the environmentVERCELin the environment- a
navigator.userAgentofCloudflare-Workers
Otherwise it chooses background. Override it if detection is wrong for your
runtime:
mg.init({ transport: "buffered" });buffered mode helps but is not a guarantee on its own. Scheduling a flush
does not mean the runtime will let it finish. That is what wrapHandler and
bindWaitUntil are for.
AWS Lambda
Section titled “AWS Lambda”import metergraph
def handler(event, context): result = do_work(event) metergraph.flush() # default timeout 3.0 seconds return resultLambda freezes the execution environment as soon as your handler returns,
so the background thread may not run again until the next invocation, if
there is one. atexit does not fire between invocations either. Flush
before you return.
import * as mg from "metergraph";
export const handler = mg.wrapHandler(async (event) => { return doWork(event);});wrapHandler awaits your handler and flushes in a finally, so rows are
delivered whether the handler returns or throws, and the original result or
error still reaches Lambda. The returned function is always async.
Cloudflare Workers
Section titled “Cloudflare Workers”Hand the platform’s own keep-alive to the SDK, once per request, and delivery continues after your response is sent.
export default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { mg.bindWaitUntil(ctx); return respond(request); },};bindWaitUntil accepts either an object with a waitUntil(promise) method,
such as the Worker ExecutionContext, or a bare waitUntil function. Once
bound, every scheduled flush is handed to it instead of being fired and
forgotten.
Binding is cheap and idempotent in effect: the last binding wins, so calling it at the top of each request is the intended usage rather than a leak.
Vercel functions
Section titled “Vercel functions”VERCEL in the environment selects buffered mode automatically. From there,
either wrap the handler or bind the platform’s waitUntil.
import * as mg from "metergraph";import { waitUntil } from "@vercel/functions";
export const POST = mg.wrapHandler(async (request: Request) => { return respond(request);});mg.bindWaitUntil(waitUntil);Use one or the other. Both together is harmless but pointless.
Long-running servers
Section titled “Long-running servers”Nothing extra. The background timer delivers on its own interval. Call
shutdown() from your existing termination handler so the last few rows are
not lost on deploy.
# atexit already calls shutdown() for you. Call it explicitly only if# your process exits by a path atexit does not cover.metergraph.shutdown()process.on("SIGTERM", async () => { await mg.shutdown(); server.close();});One-shot scripts and jobs
Section titled “One-shot scripts and jobs”shutdown() delivers queued rows and stops the background work, so a separate
flush() is not needed. Reach for flush() only when you want delivery while
Metergraph keeps running.
Forked workers
Section titled “Forked workers”Python registers a fork handler. A forked child gets a clean writer and its own background thread, and rows the parent had queued stay with the parent. So a pre-forking server such as Gunicorn works, but the queue is not inherited: each worker delivers its own rows and needs its own flush or shutdown.
When rows are dropped
Section titled “When rows are dropped”Rows are dropped rather than blocking your application. That is the deliberate trade. It happens when:
- the in-memory queue is full (
METERGRAPH_QUEUE_SIZE, default 2000) - the transport is in backoff after a failure, which doubles from 1 second up to a 60 second ceiling
- a single row is still too large after the batch has been split down to one row
- the server rejected that specific batch with a 400, 404, 413 or 422, which drops that batch only
A 401 or 403 is the exception that is not just a drop. With repository identity
configured, the session token is invalidated and a new one is fetched. Without
it, capture is disabled for the rest of the process and the SDK says so:
Metergraph authentication failed; capture disabled for this process.
Drops are counted and reported in the meta block of the next successful
batch, so the loss is visible rather than silent. If you see drops in a
serverless runtime, the usual cause is a flush that timed out. Give it a longer
timeout, and check that flush() returned true.
- Configuration for queue size, batch size and flush interval
- Limits and allowances for what the ingest endpoint accepts
- Attribution across threads for keeping a trace intact inside one invocation