Sync mode
The default mode. Keep the flag rules in sync in the background and evaluate in your process, with no request per evaluation. When to use remote mode instead.
Sync mode (mode: "sync") is the default mode of the JavaScript server SDK. The SDK downloads the complete flag rules for its environment, keeps that copy in sync with Toggld, and evaluates flags inside your process. It uses the same evaluation engine as the Toggld API, so the results are the same.
import { ToggldClient } from "@toggld/js";
const flags = await ToggldClient.create({
sdkKey: process.env.TOGGLD_SDK_KEY!,
endpoint: process.env.TOGGLD_API_URL!,
});You do not need to set mode. To send each evaluation to the API instead, pass mode: "remote". See When to use remote mode. To evaluate from a flag file on disk, pass mode: "file". See Flag files.
In @toggld/node, this mode was called "local". In @toggld/js, mode: "local" throws a TypeError. Use mode: "sync", or leave mode out.
Why sync is the default
- Fast. An evaluation takes microseconds. It does not wait for the network.
- Works offline after the first sync. If the API is unreachable, the SDK keeps evaluating with the last snapshot that it received.
- No request per evaluation. Evaluations never reach the API. They are not billable evaluation requests, and they do not use your rate limit. Remote mode sends one request for each evaluation.
Remote or sync
| Remote | Sync (default) | |
|---|---|---|
| How it evaluates | One HTTP request per evaluation | In your process, from a synchronized snapshot |
| Latency per evaluation | A network round trip | Microseconds |
| Works during an API outage | No. Calls return fallback values. | Yes, with the last snapshot it received |
| Startup | Ready immediately | Waits for the first snapshot |
| Credential | Secret key (ffk_) or public token (ffp_) | Secret key (ffk_) |
| Data on your server | Evaluated values only | All rules, variants, and referenced segments for the environment |
| Telemetry | The API counts evaluations | The SDK sends counts in batches |
| Billable requests | One per evaluation call | None |
| Best for | Public tokens, runtimes without memory between requests | Long-lived servers |
On Cloudflare Workers, Vercel, and AWS Lambda, use the serverless client. It also evaluates in your code, from a copy of the flag rules refreshed on demand. See Choosing a runtime and mode.
Sync mode needs a secret key, because the snapshot contains your targeting rules. Public tokens (ffp_) cannot download it.
What sync mode needs
- A secret key that starts with
ffk_: a server SDK key or a management key. If the key is a public token (ffp_) or is missing, or ifendpointis not anhttporhttpsURL,createthrows aTypeErrorat once. The message says what to change. - Network access to the Toggld API: HTTPS requests to
GET /sync/v1/flags, and a WebSocket connection to/sync/v1/stream. If a proxy or the runtime blocks the WebSocket, the SDK keeps working with polling only. See Transports. - A long-lived process, such as a web server or a queue worker. Synchronization runs in the background for as long as the client is open.
When to use remote mode
Pass mode: "remote" to send each evaluation to the API:
const flags = await ToggldClient.create({
sdkKey: process.env.TOGGLD_SDK_KEY!,
endpoint: process.env.TOGGLD_API_URL!,
mode: "remote",
});Use remote mode in these cases:
- Runtimes that keep no memory between requests, such as Fastly Compute. The serverless client would download the snapshot on every request there.
- Processes that must not hold your targeting rules, for example code on machines that you do not control. Remote mode receives evaluated values only.
- A public token (
ffp_). Public tokens can evaluate through the API, but they cannot download definitions.
On Cloudflare Workers, Vercel, AWS Lambda, and Netlify, the serverless client evaluates in your code instead, with no billable requests. See Choosing a runtime and mode.
A remote client is ready at once. It sends one evaluation request for each call to a getter, to evaluate, or to evaluateAll. Remote evaluations are billable requests.
The main entry checks two runtimes in sync mode:
- Cloudflare Workers and Vercel Edge:
createthrows aTypeError, because these runtimes cannot keep a stream or a poll open between requests. The message names the serverless client andmode: "remote". - AWS Lambda: the client starts, and logs one warning. Lambda freezes the process between invocations, so the stream and the poll stop, and after a freeze an evaluation can use a snapshot of any age.
Short scripts
Scripts and short-lived jobs work in any mode. In the default sync mode:
createwaits for the first snapshot, so the first evaluation already has data.- Call
close()at the end. The open stream keeps the Node.js process running until you close the client.
const flags = await ToggldClient.create({ sdkKey, endpoint });
console.log(await flags.getBooleanValue("new-checkout", false, { targetingKey: "user-123" }));
await flags.close(); // stops the sync, so the script can exitHow synchronization works
- On start, the SDK requests a snapshot from
GET /sync/v1/flags. Until the first snapshot arrives, it retries with a growing delay, from 250 ms up to 5 seconds. - With the default
wstransport, it also opens a WebSocket stream at/sync/v1/stream. The API pushes a new snapshot after each change, where the stream is available. - It keeps polling the snapshot while the stream is healthy. Polling recovers changes that the stream missed. It polls every
sync.pollIntervalMs(default 30 seconds). Against the hosted API, a healthy stream relaxes this to everysync.streamPollIntervalMs(default 5 minutes). See Poll intervals. - It replaces its snapshot only when the snapshot version changes. A poll sends the version that the SDK has in
If-None-Match. If nothing changed, the API answers304 Not Modifiedwith no body.
If a request fails, the SDK keeps its last snapshot and tries again. Evaluations continue to use the last snapshot.
Transports
sync.transport | Behavior |
|---|---|
"ws" (default) | Live stream plus polling. If the stream is not available, the SDK keeps polling and retries the stream with backoff, up to about 30 seconds between attempts. |
"poll" | Polling only, every sync.pollIntervalMs. |
Heartbeat
The stream has a heartbeat. Every 30 seconds, the SDK sends a small "ping" on the stream. If no answer comes within 10 seconds, the SDK closes the connection and reconnects. This finds a connection that a proxy or a network device dropped without notice.
The SDK sends pings only to a server that says it answers them. An older self-hosted server does not say so, and gets no pings. Its connections are never closed for a missing answer.
Poll intervals
Each snapshot on the stream says how reliably the server pushes changes:
- The hosted API promises durable delivery. It records every change that it has not pushed yet, and retries it.
- A self-hosted Node.js API promises only best-effort delivery. A process can miss a change that another process saved.
The SDK polls every sync.streamPollIntervalMs (default 5 minutes) only while the stream is healthy and the server promises durable delivery. Healthy means that the stream is open, has sent a snapshot, and has answered a heartbeat. In every other case, the SDK polls every sync.pollIntervalMs (default 30 seconds).
When the stream drops or misses a heartbeat, the SDK returns to sync.pollIntervalMs at once. If its last poll is older than that interval, it polls right away.
Readiness
ToggldClient.create waits for the first snapshot, for up to sync.readyTimeoutMs (default 5000 ms). The Effect layer waits the same way. Then it resolves even if no snapshot has arrived, and synchronization keeps trying in the background.
If the first snapshot did not arrive in time, the SDK logs one warning with the last error. For example, with a wrong key:
[toggld] No flag snapshot after 5000 ms. Evaluations return your fallback values (PROVIDER_NOT_READY) until the first sync succeeds; sync keeps retrying in the background. Last error: The API rejected the SDK key (HTTP 401). Check that `sdkKey` is a valid, unrevoked key.Until the first snapshot arrives:
- Typed getters return your fallback value.
- The details report
reason: "ERROR"anderrorCode: "PROVIDER_NOT_READY". TheerrorMessagenames the last synchronization error, if there was one. evaluateAllreturns an empty array.
To wait for the first snapshot without a time limit, call ready():
const flags = await ToggldClient.create({ sdkKey, endpoint });
await flags.ready(); // resolves when the first snapshot arrivesready() never resolves if the key is wrong, so add your own timeout if you use it at startup. In remote mode, ready() resolves at once.
How fast changes arrive
A saved change does not reach every SDK at once. Use these numbers to plan.
Sync mode on the hosted API. A change usually arrives as a stream push within moments. If the API fails to send a push, a retry runs every minute and sends it, about 10 to 70 seconds after the change. A connection that died without notice is found within 40 seconds. Then the SDK polls and reconnects, and the new connection sends the current snapshot. The hosted API can serve a polled snapshot from an edge cache for up to 60 seconds after a change. A polled snapshot never replaces a newer one that the stream already sent: each snapshot carries an ordered config version, and the SDK keeps the newer one. Without a working stream, plan for sync.pollIntervalMs, plus up to 60 seconds of cache age, plus request time. Toggld does not promise a fixed delivery time.
Remote mode on the hosted API. Each evaluation reads the API. The same edge cache can serve flag data for up to 60 seconds after a change.
Sync mode on a self-hosted Node.js API. A change pushes to the SDKs that are connected to the same server process that saved it. Other processes learn about the change through polling, so the SDK always polls every sync.pollIntervalMs. With successful requests, the conservative delivery bound is pollIntervalMs + 2 × timeoutMs, plus scheduling delays. With the defaults, that is 40 seconds. All API processes must read the same Postgres database. If you read from a replica, add its replication delay.
During network outages or API failures, there is no fixed delivery bound. The SDK converges after requests succeed again.
Get updates faster
Lower sync.pollIntervalMs to poll more often:
const flags = await ToggldClient.create({
sdkKey, endpoint,
sync: { pollIntervalMs: 10_000 },
});Each poll downloads a full snapshot. Snapshot polls and remote evaluations with secret keys share one rate limit of 1,200 requests per minute per environment. That budget covers every client that uses a secret key in the environment. For example, 100 sync-mode clients that poll every 10 seconds use 600 requests per minute. Use a positive, finite interval.
Performance
A loopback benchmark on a laptop compared the two modes with the same workload:
| Mode | Throughput | Median latency |
|---|---|---|
| Remote (OFREP over HTTP) | about 11,600 evaluations per second | 2.4 ms |
| Sync (in-process engine) | about 1,090,000 evaluations per second | 27 µs |
These numbers come from a local test, not from the hosted API. Over the internet, remote evaluation also pays the network round trip.
Segments
The snapshot includes the segments that your flags reference, with their rules and member lists. Segment membership resolves in your process.
API runtimes
The Toggld API runs in two runtimes, and synchronization differs between them:
| API runtime | Stream | Updates |
|---|---|---|
Cloudflare Workers (the hosted API at api.toggld.io) | Available, durable delivery | Stream pushes, plus a poll every 5 minutes while the stream is healthy |
| Node.js (self-hosted) | Available, best-effort delivery | Stream pushes, plus a poll every 30 seconds |
The main entry targets Node.js 22 or later. It uses Node.js APIs, such as node:fs for flag files. On runtimes that do not keep a process running, use the serverless client, which needs no Node.js APIs. See Choosing a runtime and mode.
Telemetry
In sync mode, the SDK counts evaluations and sends the counts in batches every 30 seconds and on close(). These counts feed Analytics. Remote mode sends no SDK telemetry, because the API counts those evaluations. File mode sends no telemetry. See Telemetry.
Evaluate from a file
File mode (mode: "file") reads the snapshot from a file instead of the network. Use it for local development, tests, and CI. A file-mode client needs no key and no network. The TOGGLD_FLAGS_FILE environment variable switches a client in any mode to file mode. See Flag files.
Choosing a runtime and mode
Which client to use on Node.js servers, Cloudflare Workers, Vercel, AWS Lambda, and other runtimes, and what each mode costs.
Serverless client
Evaluate flags in your code on Cloudflare Workers, Vercel, AWS Lambda, and other request-scoped runtimes with @toggld/js/serverless.