# 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

```ts
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
  unconditional `finally` covers 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:

```ts
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`:

```ts
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:

```ts
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](/docs/frameworks/laravel#delivery-the-terminating-phase)**.

## 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](/docs/sdks/javascript)** — the base guide and the full method surface.
- **[Next.js](/docs/frameworks/nextjs)** · **[Nuxt](/docs/frameworks/nuxt)** ·
  **[SvelteKit](/docs/frameworks/sveltekit)** — meta-frameworks that commonly deploy here.
- **[API Reference](/docs/api-reference)** — every config option and the HTTP contract.
