Cloudflare
Cloudflare integration is typically implemented with a Worker that calls Fairvisor before forwarding traffic to your origin.
Integration patterns
| Pattern | When to use |
|---|---|
| Worker decision check (recommended) | You need strict control over allow/reject handling and header propagation |
| Fairvisor as origin proxy | You want Cloudflare to route directly to Fairvisor in reverse_proxy mode |
Pattern 1 — Worker decision check (recommended)
Flow:
- Worker receives request
- Worker calls
POST /v1/decision 200: forward to origin429: return reject withRetry-After,X-Fairvisor-Reason, andRateLimit*
export default {
async fetch(request, env) {
const url = new URL(request.url);
const originalUri = url.pathname + url.search;
const decisionHeaders = new Headers();
decisionHeaders.set("X-Original-Method", request.method);
decisionHeaders.set("X-Original-URI", originalUri);
const auth = request.headers.get("authorization");
if (auth) decisionHeaders.set("Authorization", auth);
const clientIp = request.headers.get("cf-connecting-ip");
if (clientIp) decisionHeaders.set("X-Forwarded-For", clientIp);
const decisionRes = await fetch(`${env.FAIRVISOR_EDGE_URL}/v1/decision`, {
method: "POST",
headers: decisionHeaders,
});
if (decisionRes.status === 429) {
const headers = new Headers();
for (const k of [
"retry-after",
"x-fairvisor-reason",
"ratelimit",
"ratelimit-limit",
"ratelimit-remaining",
"ratelimit-reset",
]) {
const v = decisionRes.headers.get(k);
if (v) headers.set(k, v);
}
return new Response("Too Many Requests", { status: 429, headers });
}
if (!decisionRes.ok) {
// Fail policy choice:
// - fail-closed: return 503
// - fail-open: continue to origin
return new Response("Decision service unavailable", { status: 503 });
}
const upstream = await fetch(new Request(request, { cf: { cacheEverything: false } }));
return upstream;
}
};
Required env vars
FAIRVISOR_EDGE_URL=https://fairvisor-edge.internal.example.com
Keep Fairvisor reachable only from trusted network paths (for example private origin network or tunnel), not public internet.
Pattern 2 — Fairvisor as reverse-proxy origin
Set Cloudflare origin to Fairvisor Edge, run edge in reverse_proxy mode:
FAIRVISOR_MODE=reverse_proxy
FAIRVISOR_BACKEND_URL=http://your-origin-service:3000
FAIRVISOR_CONFIG_FILE=/etc/fairvisor/policy.json
In this setup, every request to origin is enforced inline by Fairvisor.
Failure policy
Cloudflare pattern should explicitly choose fail-open or fail-closed:
- Fail-closed: safer for sensitive API routes
- Fail-open: better availability for low-risk/public routes
Recommended split is documented in Gateway Failure Policy.
Timeout and retry guidance
- Keep decision timeout low (typically 200-500 ms budget)
- Avoid repeated retries on the hot path
- Emit logs/metrics for every fallback decision
Header mapping checklist
When calling Fairvisor, include:
X-Original-MethodX-Original-URIAuthorization(if used for JWT descriptors)X-Forwarded-For(fromcf-connecting-ip)
Verification
# readiness
curl -i http://fairvisor-edge.internal.example.com:8080/readyz
# direct decision probe
curl -i -X POST http://fairvisor-edge.internal.example.com:8080/v1/decision \
-H "X-Original-Method: GET" \
-H "X-Original-URI: /v1/chat/completions" \
-H "X-Forwarded-For: 198.51.100.42"
Look for 429 rejects with X-Fairvisor-Reason and Retry-After.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Worker always returns 503 | Fairvisor unreachable or timeout too strict | Verify network path and adjust timeout budget |
| Fairvisor sees wrong route | Missing or malformed X-Original-URI |
Forward full path + query |
| Per-user limits collapse into one bucket | Missing identity headers/JWT | Forward Authorization and required descriptor headers |
See Decision API for the full contract.