Flags guides
07 · Fail-safe and caching

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.

Fail-safe order
#SourceWhen
1A fresh downloadEvery poll; a 304 when nothing changed.
2The config the client holdsA failed poll keeps it (reason STALE until the next success).
3Your cacheRead at start-up, written after every download. Survives restarts.
4The bootstrapThe config JSON your build shipped with.
5Your defaultThe 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

lib/flags.ts
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
// 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

hot-path.ts
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 }));
}