Serverless & edge
Lambda, Vercel Functions, Cloudflare Workers, Deno Deploy — the rule is one line:
There is no process lifecycle hook here, so the queue will not drain itself. Await the flush before you return.
The runtime may freeze or kill your invocation the instant you hand back a response. A report that is still sitting in the queue at that moment is simply gone, and nothing tells you so.
AWS Lambda
import { createClient } from 'bugboard'; // Module scope: created once per container, reused across warm invocations. const bugboard = createClient({ keyId: process.env.BUGBOARD_KEY_ID, // bbk_… signingSecret: process.env.BUGBOARD_SIGNING_SECRET, // bb_sec_… environment: process.env.STAGE, }); export const handler = async (event) => { try { return { statusCode: 200, body: JSON.stringify(await process(event)) }; } catch (err) { bugboard.criticalHigh('Lambda handler failed', err, ['lambda']); return { statusCode: 500, body: '{"error":"Internal Server Error"}' }; } finally { await bugboard.flush(); } };
Two things to copy from this shape:
- Module scope is right. The client is cheap to construct, but there is no reason to rebuild it per invocation and a warm container reuses it. Because the drain timer only runs while the queue is non-empty, an idle client between invocations schedules nothing and won't hold the container awake.
finally, not the catch block.flush()on an empty queue is effectively free, so an unconditionalfinallycovers both paths and you never have to track whether you reported anything.
Cloudflare Workers
Use ctx.waitUntil() so the response isn't delayed by the flush:
export default { async fetch( request: Request, env: Env, ctx: ExecutionContext, ): Promise<Response> { const bugboard = createClient({ keyId: env.BUGBOARD_KEY_ID, signingSecret: env.BUGBOARD_SIGNING_SECRET, environment: env.ENVIRONMENT, }); try { return await handleRequest(request); } catch (err) { bugboard.criticalHigh('Worker request failed', err, ['workers']); return new Response('Internal Server Error', { status: 500 }); } finally { ctx.waitUntil(bugboard.flush()); // response goes out now; flush finishes after } }, };
In Workers the client must be created inside fetch — bindings like env aren't available
at module scope. This is the one place the "build it once at module scope" rule doesn't apply.
Vercel Edge Functions
Same idea, with waitUntil from @vercel/functions:
import { waitUntil } from '@vercel/functions'; export const config = { runtime: 'edge' }; export default async function handler(request: Request) { try { return await handle(request); } catch (err) { bugboard.criticalHigh('Edge function failed', err, ['vercel', 'edge']); return new Response('Error', { status: 500 }); } finally { waitUntil(bugboard.flush()); } }
waitUntil is strictly better than await where it exists: the response goes out immediately
while the runtime keeps the invocation alive long enough for delivery to finish.
Deno Deploy
No waitUntil equivalent — plain await bugboard.flush() before returning.
Tuning for pay-per-millisecond runtimes
The SDK defaults suit a long-running server. On serverless you're paying wall-clock time for the flush, so cap the worst case:
createClient({ keyId: process.env.BUGBOARD_KEY_ID, signingSecret: process.env.BUGBOARD_SIGNING_SECRET, timeoutMs: 2000, maxRetries: 1, // 3 retries × backoff can add seconds to every invocation flushIntervalMs: 500, // less left to drain when the explicit flush arrives });
With the defaults, a fully unreachable endpoint costs up to ~4 attempts × 5 s plus backoff on
the invocation that happens to be flushing. maxRetries: 1 trades a little delivery
reliability for a bounded tail — usually the right trade when you're billed by the
millisecond.
PHP on serverless
The same rule applies to PHP deployed to Lambda (Bref) or any freeze-after-response runtime:
the SDK's register_shutdown_function hook may not run, or may run after the runtime has
already frozen. Call flush() explicitly at the end of the handler.
Under Laravel on a serverless target, the terminating phase still runs per request, so
reports go out normally — but if you ever see one arriving an invocation late, add an explicit
BugBoard::flush(). See Laravel.
Why the SDK can't do this for you
Every automatic hook the SDK has — Node's beforeExit, the browser's pagehide, PHP's
register_shutdown_function — depends on the runtime telling it that things are ending.
Serverless runtimes don't: they freeze the whole world between invocations and may never
resume it. There is no event to listen for, which is why the explicit flush is the one piece of
ceremony these environments require.
Next steps
- JavaScript SDK — the base guide and the full method surface.
- Next.js · Nuxt · SvelteKit — meta-frameworks that commonly deploy here.
- API Reference — every config option and the HTTP contract.