Keep working when we don’t.
What your app evaluates when Flags is slow or down, and the three lines of setup that make an outage a non-event.
1
The chain
The client takes the first of these it has, in this order.
| # | Source | When |
|---|---|---|
| 1 | A fresh download | Every poll; a 304 when nothing changed. |
| 2 | The config the client holds | A failed poll keeps it (reason STALE until the next success). |
| 3 | Your cache | Read at start-up, written after every download. Survives restarts. |
| 4 | The bootstrap | The config JSON your build shipped with. |
| 5 | Your default | The second argument of getValue (reason ERROR). |
A running process already holds a config, so it carries on through an outage of any length with the last config it saw. The cache and the bootstrap are for processes that start during an outage: a deploy, an autoscaling event, a crash loop.
2
Set it up
import { FlagsClient, type FlagsCache } from "@productcraft/flags";
import Redis from "ioredis";
import bootstrap from "./flags-bootstrap.json";
const redis = new Redis(process.env.REDIS_URL!);
const cache: FlagsCache = {
get: (key) => redis.get(key),
set: async (key, value) => {
await redis.set(key, value);
},
};
export const flags = new FlagsClient({
sdkKey: process.env.PRODUCTCRAFT_FLAGS_SDK_KEY,
polling: { mode: "auto", intervalSeconds: 60, maxInitWaitSeconds: 5 },
cache,
bootstrap,
});
flags.on("error", (err) => console.warn("flags", err.code, err.message)); // never contains the key
flags.on("ready", (state) => console.info("flags", state)); // ready | stale | key_invalid …The first getValue waits up to maxInitWaitSeconds (5 by default) for the first download, then answers from the cache, the bootstrap or your default. The cache keys never contain the SDK key, and the SDK never logs it.
3
Write the bootstrap in CI
// scripts/flags-bootstrap.ts: run in CI before the build
import { writeFileSync } from "node:fs";
import { FlagsClient } from "@productcraft/flags";
const flags = new FlagsClient({
sdkKey: process.env.PRODUCTCRAFT_FLAGS_SDK_KEY,
polling: { mode: "manual" },
});
await flags.forceRefresh();
const config = flags.snapshot().toJSON();
if (!config) throw new Error("no config downloaded");
writeFileSync("flags-bootstrap.json", JSON.stringify(config));
flags.close();A stale bootstrap is harmless: the SDK replaces it with any newer config it finds, and never moves back to an older revision.
4
On our side
- Config delivery keeps serving from memory when our database is down, marked
X-PC-Config-Stale: 1, for up to 24 hours. - Turning Flags off for a workspace blocks the management API but keeps delivery running.
- Responses allow shared caches (
s-maxage=15,stale-if-error=3600), so a proxy of your own in front of the delivery URL can serve through a blip; it adds up to 15 s to propagation. - The SDK refuses a body over 2 MiB and only follows delivery hints to
https://*.productcraft.co.
5
Hot paths: evaluate without await
await flags.waitForReady();
// A frozen view of the config held right now: no await per call.
const snap = flags.snapshot();
for (const id of ["user-1", "user-2", "user-3"]) {
console.log(id, snap.getValue("new_checkout", false, { identifier: id }));
}