Dashboard
JavaScript (server)

Serverless client

Evaluate flags in your code on Cloudflare Workers, Vercel, AWS Lambda, and other request-scoped runtimes with @toggld/js/serverless.

@toggld/js/serverless evaluates flags in your code on runtimes that run code only while they handle a request: Cloudflare Workers, Vercel Functions, AWS Lambda, and others. To find the right client for your runtime, see Choosing a runtime and mode.

The client keeps the flag snapshot of its environment in memory, and evaluates in your code with the same engine as the Toggld API. It holds no socket, timer, or background loop. A request refreshes the snapshot when the snapshot gets old, so the docs call this strategy on-demand. It has no mode option.

  • No billable requests. Evaluations run in your code. Snapshot refreshes are definition sync, which is fair use. See Plans and limits.
  • Fast. Most evaluations take microseconds. Only the first request of an isolate or process waits for the snapshot.
  • Works without Node.js APIs. The entry imports no node: modules, so it runs on Cloudflare Workers without the nodejs_compat flag.

Install

pnpm add @toggld/js

The serverless client needs a secret key that starts with ffk_, because it downloads your targeting rules. Store it as a secret of your platform, such as wrangler secret put TOGGLD_SDK_KEY.

Cloudflare Workers

Create one client per isolate, and one request view per request with forRequest(ctx):

src/index.ts
import { createServerlessClient, type ServerlessClient } from "@toggld/js/serverless";

let flags: ServerlessClient | undefined;

export default {
  async fetch(request, env, ctx): Promise<Response> {
    flags ??= createServerlessClient({ sdkKey: env.TOGGLD_SDK_KEY, endpoint: "https://api.toggld.io" });
    const f = flags.forRequest(ctx);
    const on = await f.getBooleanValue("new-checkout", false, { targetingKey: "user-123" });
    return new Response(on ? "new checkout" : "old checkout");
  },
} satisfies ExportedHandler<Env>;

createServerlessClient makes no request, so you can also call it in module scope, for example with import { env } from "cloudflare:workers".

With ctx, the client refreshes a stale snapshot and sends telemetry with ctx.waitUntil, after your response.

Vercel and Next.js

On Vercel Edge Functions and Next.js middleware, pass an object with waitUntil:

middleware.ts
import { waitUntil } from "@vercel/functions";
import { createServerlessClient } from "@toggld/js/serverless";

const flags = createServerlessClient({
  sdkKey: process.env.TOGGLD_SDK_KEY!,
  endpoint: process.env.TOGGLD_API_URL!,
});

export async function middleware(request: Request) {
  const f = flags.forRequest({ waitUntil });
  const on = await f.getBooleanValue("new-checkout", false, { targetingKey: "user-123" });
  // …
}

AWS Lambda and Node.js functions

On AWS Lambda, Vercel Node.js functions, and Netlify Functions, call forRequest() without a context:

handler.ts
import { createServerlessClient } from "@toggld/js/serverless";

const flags = createServerlessClient({
  sdkKey: process.env.TOGGLD_SDK_KEY!,
  endpoint: process.env.TOGGLD_API_URL!,
});

export const handler = async (event: { userId: string }) => {
  const f = flags.forRequest();
  const on = await f.getBooleanValue("new-checkout", false, { targetingKey: event.userId });
  return { statusCode: 200, body: on ? "new checkout" : "old checkout" };
};

These platforms freeze the process as soon as the handler returns, so work that runs after the response is not guaranteed to finish. Without a context, the client does all of its work inside the call:

  • A stale snapshot is refreshed before the evaluation returns. The wait is bounded by timeoutMs. If the refresh fails, the client uses the stale snapshot.
  • When telemetry is due, it is sent before the evaluation returns, with a limit of 1 second. A failed upload never fails the evaluation. The counts are sent with the next upload.

An object without a waitUntil method, such as the Lambda context, counts as no context.

Freshness

Each request checks the age of the snapshot in memory:

Snapshot ageWith waitUntilWithout waitUntil
Younger than maxAgeMs (default 30 seconds)Use it.Use it.
Younger than maxStaleMs (default 5 minutes)Use it, and refresh it after the response. Only one refresh runs per isolate at a time.Refresh it, then evaluate.
No snapshot yet, or older than maxStaleMsRefresh it, then evaluate.Refresh it, then evaluate.
  • A refresh is bounded by timeoutMs (default 1500 ms).
  • If a refresh fails, the client uses the snapshot that it has, however old. An unreachable API never turns your flags off. Without any snapshot, evaluations return your fallback value with errorCode: "PROVIDER_NOT_READY", and errorMessage names the last error. After a failure, the client waits before it tries again, from 1 second up to 30 seconds, or for the Retry-After time of a 429.
  • An unchanged snapshot costs little. A refresh sends If-None-Match with the version that the client has. If nothing changed, the API answers 304 Not Modified with no body.
  • A newer snapshot is never replaced by an older one. The API's cache can serve an older snapshot than one that the client already has. The client keeps the newer one.
  • All evaluations of one request view use one check. A request that evaluates ten flags checks the snapshot age once.

A change reaches an isolate or process within about maxAgeMs plus up to 60 seconds of API cache age, once it receives a request. Each isolate or process has its own snapshot, so two of them can briefly serve different values. A new isolate or process waits for its first snapshot. If many requests reach a new isolate at once, each of them can download the snapshot once.

Evaluate flags

The request view has the same evaluation methods as the main client, and they return the same details, reasons, and error codes. All of them are async.

const f = flags.forRequest(ctx);
const theme = await f.getStringValue("checkout-theme", "light", { targetingKey: "user-123" });
const details = await f.getBooleanDetails("new-checkout", false, { targetingKey: "user-123" });
const all = await f.evaluateAll({ targetingKey: "user-123" });

Use a request view only inside the request that created it. On Cloudflare Workers, I/O that one request starts cannot be used by another request, so the client never shares a pending download between requests.

Effect API

@toggld/js/serverless/effect provides the serverless client as the same ToggldClient service that @toggld/js/effect provides. Program code that uses yield* ToggldClient works with either layer. Install effect (version 4) next to the SDK. This entry imports no node: modules either.

Build the layer once per isolate or process, for example in a module-scope ManagedRuntime. Each evaluation looks for the optional WaitUntil service. Provide it per request with withWaitUntil(ctx). Without it, the client works as without a context: it refreshes a stale snapshot inline.

src/index.ts
import { Effect, ManagedRuntime } from "effect";
import { ToggldClient, layer, withWaitUntil } from "@toggld/js/serverless/effect";

const runtime = ManagedRuntime.make(
  layer({ sdkKey: "ffk_YOUR_SECRET_KEY", endpoint: "https://api.toggld.io" }),
);

const checkout = Effect.gen(function* () {
  const flags = yield* ToggldClient;
  const on = yield* flags.getBooleanValue("new-checkout", false, { targetingKey: "user-123" });
  return new Response(on ? "new checkout" : "old checkout");
});

export default {
  fetch: (request, env, ctx) => runtime.runPromise(checkout.pipe(withWaitUntil(ctx))),
} satisfies ExportedHandler<Env>;

On AWS Lambda, run the program without withWaitUntil.

  • withWaitUntil(ctx) calls ctx.waitUntil as a method of ctx, as Workers requires. waitUntilLayer(ctx) gives the same service as a layer.
  • ready runs the freshness check that an evaluation would run, and completes even if the refresh fails.
  • flushTelemetry sends buffered counts within the current request.
  • If you interrupt an evaluation that waits for a refresh, the refresh request is cancelled. A refresh that runs in waitUntil is not cancelled.
  • Closing the layer's scope sends the buffered telemetry, bounded by timeoutMs. The client holds no other resource.
  • Options that can never work, such as a public token, make the layer fail with a TypeError.

Options

OptionDefaultDescription
sdkKeyRequiredA secret key (ffk_). A public token (ffp_) throws a TypeError.
endpointRequiredThe API URL, such as https://api.toggld.io.
maxAgeMs30000How long a snapshot is used before a request refreshes it.
maxStaleMs300000How old a snapshot can be before a request waits for a refresh.
timeoutMs1500The timeout of each request to the API.
defaultContextNoneAttributes merged into every evaluation context. The per-call context wins.
telemetryOnfalse turns telemetry off. { flushIntervalMs } sets how often counts are sent (default 30 seconds).
fetchGlobal fetchA custom fetch implementation.

Telemetry

The client counts evaluations in memory and sends the counts to the API at most once per flushIntervalMs, from a request: with waitUntil after the response, without it inside the call. The counts feed Analytics. Counts that are not sent yet are lost if the platform discards the isolate or process. To send them at a known point, such as the end of a handler, call await f.flushTelemetry().

Limits

  • One snapshot per isolate or process. Memory use grows with the size of your flag set, as in sync mode.
  • No live updates. Changes arrive on the next refresh, not as pushes. For live updates, use sync mode on a long-lived server.
  • Secret keys only. For a public token, use @toggld/browser in the browser, or remote mode on the server.
  • No flag files. To evaluate from a file, use the main entry. See Flag files.

On this page