This repository is an experiment focused on comparing JavaScript and Rust Cloudflare Worker implementations of OFREP (OpenFeature Remote Evaluation Protocol).
The goal is to document and measure:
- Implementation challenges (runtime compatibility, dependency constraints, and packaging ergonomics)
- Performance characteristics (single-flag and bulk evaluation behavior)
- Practical trade-offs between maintainability, portability, and throughput/latency
After comparing JavaScript and Rust Cloudflare Worker implementations, we are moving forward with the JavaScript OFREP Worker in the OpenFeature repo.
Why:
- Rust Workers have slower cold starts in this setup due to the WASM loading/initialization overhead versus JavaScript.
- Runtime evaluation performance differences were not noticeable enough to justify the Rust complexity for this use case.
- The JavaScript JSONLogic ecosystem used here is more mature and simpler to operate today.
Outcome:
- JavaScript Worker is the default path for upstream adoption.
- This experiment also proves a Rust-based Cloudflare Worker is viable and remains a valid future option as constraints/performance characteristics evolve.
JS flagd-core worker compatibility tasks and required changes:
- Add a worker-safe runtime mode
- Introduce
workers?: booleanoption onFlagdCoreso worker behavior is explicit and opt-in.
- Introduce
- Remove runtime code generation paths
- Switch JSONLogic evaluation from compiled mode (
.build()) to interpreter mode (.run()) whenworkers: true.
- Switch JSONLogic evaluation from compiled mode (
- Pre-generate schema validators at build time
- Use
ajv-standaloneto generate static validators and avoidnew Function()during worker startup.
- Use
- Thread options through core evaluation pipeline
- Ensure parser/storage/targeting construction propagates worker mode consistently.
- Add generated artifacts + build script wiring
- Check in generated validator module and keep generation script part of build/release workflow.
- Validate behavior + performance baseline
- Add compatibility tests and benchmark checks to confirm OFREP parity and acceptable latency in worker environments.
- Upstream rollout steps
- Land changes in
js-sdk-contrib, consume from JS CF worker package, then promote to OpenFeature once stabilized.
- Land changes in
This project enables feature flag evaluation entirely within Cloudflare Workers using the flagd evaluation engine. It includes three experimental implementations:
- JavaScript Worker: Uses a Workers-compatible fork of
@openfeature/flagd-core - Rust Worker: Uses the flagd Rust SDK compiled to native WebAssembly
- Rust Forking Worker: Uses the standalone
forking-flagd-evaluatorcrate (WASM-first design)
All implementations expose the same OFREP API, allowing clients to evaluate flags via HTTP so behavior and performance can be compared with minimal API differences.
┌─────────────────────────────────────────────────────────────────┐
│ Cloudflare Worker │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ OFREP Handler │ │
│ │ POST /ofrep/v1/evaluate/flags/{key} → Single eval │ │
│ │ POST /ofrep/v1/evaluate/flags → Bulk eval │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ flagd Evaluation Engine │ │
│ │ • JSONLogic targeting rules │ │
│ │ • Fractional evaluation (percentage rollouts) │ │
│ │ • Semantic version comparison │ │
│ │ • String operations (starts_with, ends_with) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Flag Configuration │ │
│ │ (bundled JSON at build time) │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
flagd-ofrep-cf-worker/
├── packages/
│ ├── js-ofrep-worker/ # Reusable JS package
│ ├── rust-ofrep-worker/ # Reusable Rust crate (uses rust-sdk-contrib)
│ └── rust-ofrep-worker-forking/ # Reusable Rust crate (uses flagd-evaluator)
├── examples/
│ ├── js-worker/ # JS Cloudflare Worker example
│ ├── rust-worker/ # Rust Cloudflare Worker example
│ └── rust-worker-forking/ # Rust Cloudflare Worker (forking) example
├── contrib/
│ ├── js-sdk-contrib/ # Git submodule: forked JS flagd-core
│ ├── rust-sdk-contrib/ # Git submodule: forked Rust flagd SDK
│ └── flagd-evaluator/ # Git submodule: forked flagd-evaluator
└── docs/ # Documentation
The standard @openfeature/flagd-core package cannot run in Cloudflare Workers because it depends on libraries that use dynamic code generation:
| Library | Usage | Problem |
|---|---|---|
ajv |
JSON Schema validation | Uses new Function() to compile validators at module load time |
json-logic-engine |
Targeting rule evaluation | Uses eval() in .build() compilation mode |
Cloudflare Workers run in V8 isolates with strict security restrictions that block eval() and new Function().
This repository includes a fork of @openfeature/flagd-core (as a git submodule) with an optional workers compatibility mode that:
- Pre-compiled ajv validators: Generated at build time using
ajv-standalone, avoiding runtime code generation - Interpreter mode for JSONLogic: Uses
.run()instead of.build(), which interprets rules without code generation
The fork is maintained at DevCycleHQ-Sandbox/js-sdk-contrib on the feat/workers-compatibility branch.
Files modified in libs/shared/flagd-core/:
| File | Changes |
|---|---|
src/lib/options.ts |
New FlagdCoreOptions interface with workers?: boolean |
src/lib/targeting/targeting.ts |
Conditionally uses .run() interpreter when workers: true |
src/lib/parser.ts |
Uses pre-compiled validators when workers: true |
src/lib/feature-flag.ts |
Passes options through to Targeting |
src/lib/storage.ts |
Passes options through to parser |
src/lib/flagd-core.ts |
Accepts FlagdCoreOptions in constructor |
scripts/build-validators.js |
Generates pre-compiled ajv validators |
src/lib/generated/validators.js |
Auto-generated pre-compiled validators |
import { FlagdCore } from '@openfeature/flagd-core';
// For Cloudflare Workers (no dynamic code generation)
const core = new FlagdCore(undefined, undefined, { workers: true });
// For Node.js (default, uses compilation for better performance)
const core = new FlagdCore();| Mode | JSONLogic | Performance | Environment |
|---|---|---|---|
workers: false (default) |
Compiled (.build()) |
~10-20x faster | Node.js |
workers: true |
Interpreted (.run()) |
Slower but compatible | Cloudflare Workers |
For typical OFREP usage (a few flag evaluations per request), the interpreter mode is fast enough. The absolute times are still in microseconds.
# Install dependencies
npm install
# Build packages
npm run build
# Run the JS worker
npm run dev:jsimport { createOfrepHandler } from '@openfeature/flagd-ofrep-cf-worker';
import flags from './flags.json';
const handler = createOfrepHandler({ flags });
export default {
fetch: handler,
};See packages/js-ofrep-worker/README.md for full documentation.
The standard flagd Rust SDK (open-feature-flagd crate) cannot compile to WASM for Cloudflare Workers.
Important clarification: Cloudflare Workers do support async Rust via wasm-bindgen-futures, which bridges Rust Futures to JavaScript Promises. The workers-rs crate uses this automatically. The problem is specifically tokio, not async in general.
The open-feature Rust crate unconditionally depends on tokio, which depends on mio for I/O polling:
open-feature v0.2.7
└── tokio v1.49.0
└── mio v1.1.1 ← Uses system calls (epoll/kqueue) not available in WASM
Even with --no-default-features, the open-feature crate still pulls in tokio. This is a limitation of the upstream crate design.
| Dependency | Usage | Problem |
|---|---|---|
tokio |
Async runtime | Uses mio for I/O which requires system calls |
open-feature crate |
OpenFeature SDK | Unconditionally depends on tokio |
mio |
I/O polling | Uses epoll/kqueue/etc. not available in WASM |
Since we cannot use the open-feature crate at all (it unconditionally pulls in tokio), we created a fork of the flagd Rust SDK with a new wasm feature that bypasses it entirely:
- Makes
open-featuredependency optional: The core evaluation logic doesn't need the full OpenFeature SDK - Creates
WasmEvaluationContext: A lightweight context type replacingopen_feature::EvaluationContext - Adds
SimpleFlagStore: Synchronous flag evaluation usingserde_json::Valuedirectly - Gates async code: All tokio-dependent code is behind
#[cfg(feature = "tokio")]
The evaluation logic itself is synchronous (JSONLogic rules, fractional rollouts, etc.), so avoiding tokio doesn't limit functionality - it just requires alternative types for the evaluation context.
The fork is maintained at DevCycleHQ-Sandbox/rust-sdk-contrib on the feat/wasm-support branch.
Key changes to the flagd crate:
| File | Changes |
|---|---|
Cargo.toml |
Added wasm feature, made open-feature optional |
src/lib.rs |
Gated FlagdProvider, FlagdOptions behind #[cfg(feature = "tokio")] |
src/wasm_context.rs |
New WasmEvaluationContext type for WASM environments |
src/resolver/in_process/simple_store.rs |
New SimpleFlagStore for sync evaluation |
src/resolver/in_process/targeting/mod.rs |
Updated to use WasmEvaluationContext in WASM mode |
src/resolver/in_process/model/mod.rs |
Gated value_converter module |
src/error.rs |
Added FlagNotFound, FlagDisabled error variants |
┌─────────────────────────────────────────────────────────────────┐
│ Rust Worker (WASM) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ rust-ofrep-worker crate │ │
│ │ • OfrepHandler - HTTP request/response handling │ │
│ │ • OFREP types (Request, Response, Error) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ open-feature-flagd (wasm feature) │ │
│ │ • SimpleFlagStore - sync flag evaluation │ │
│ │ • WasmEvaluationContext - lightweight context │ │
│ │ • No tokio, no open-feature crate dependencies │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Pure Rust Dependencies │ │
│ │ • datalogic-rs - JSONLogic evaluation │ │
│ │ • murmurhash3 - fractional rollout bucketing │ │
│ │ • semver - semantic version comparison │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
- Native WASM: Compiles to WebAssembly bytecode, no
new Function()restrictions - Smaller bundle: ~562 KB gzipped vs JS implementation
- Type safety: Full Rust type checking at compile time
- Same flag format: Uses identical flagd JSON configuration
Prerequisites:
- Rust toolchain via
rustup wasm32-unknown-unknowntarget:rustup target add wasm32-unknown-unknown- Node.js 25+ (for wrangler)
# Build the Rust worker
cd examples/rust-worker
npx wrangler build
# Run locally
npx wrangler dev --port 8788use rust_ofrep_worker::{OfrepHandler, OfrepRequest};
use worker::*;
const FLAGS_JSON: &str = include_str!("flags.json");
#[event(fetch)]
async fn main(mut req: Request, _env: Env, _ctx: Context) -> Result<Response> {
let handler = OfrepHandler::new(FLAGS_JSON)?;
let body: OfrepRequest = req.json().await.unwrap_or_default();
match handler.evaluate_flag("my-flag", &body) {
Ok(result) => Response::from_json(&result),
Err(error) => Response::from_json(&error),
}
}See packages/rust-ofrep-worker/README.md for full documentation.
The forking-flagd-evaluator is a standalone WASM-first evaluator originally designed for embedding in Java applications via Chicory. It provides an alternative to the rust-sdk-contrib approach with a cleaner, purpose-built architecture.
However, it includes a WASM host import for getting the current time (used for $flagd.timestamp context enrichment):
#[link(wasm_import_module = "host")]
extern "C" {
fn host_get_current_time() -> u64;
}This causes esbuild to fail when bundling for Cloudflare Workers:
Could not resolve "host"
index.js:428:25:
428 │ import * as import1 from "host"
In Chicory, the Java host provides this module. In Cloudflare Workers, there's no equivalent - we need to use JavaScript's Date.now() instead.
We maintain a fork that adds a js-time feature flag. When enabled, this feature:
- Disables the host import: The
extern "C"block is conditionally compiled out - Uses JavaScript time: Calls
js_sys::Date::now()instead of the host function
The feature is additive - default builds still work with Chicory, but enabling js-time makes it Cloudflare Workers compatible.
The fork is maintained at DevCycleHQ-Sandbox/flagd-evaluator on the feat/js-time-feature branch.
Key changes:
| File | Changes |
|---|---|
Cargo.toml |
Added js-time feature, optional js-sys dependency |
src/lib.rs |
Gated host import behind #[cfg(not(feature = "js-time"))] |
src/lib.rs |
Added js_sys::Date::now() path when js-time enabled |
Cargo.toml changes:
[features]
default = []
js-time = ["js-sys"]
[dependencies]
js-sys = { version = "0.3", optional = true }Conditional compilation in lib.rs:
// Host import only for Chicory (non-js-time builds)
#[cfg(all(target_family = "wasm", not(feature = "js-time")))]
#[link(wasm_import_module = "host")]
extern "C" {
fn host_get_current_time() -> u64;
}
pub fn get_current_time() -> u64 {
#[cfg(all(target_family = "wasm", feature = "js-time"))]
{ (js_sys::Date::now() / 1000.0) as u64 }
#[cfg(all(target_family = "wasm", not(feature = "js-time")))]
{ unsafe { host_get_current_time() } }
#[cfg(not(target_family = "wasm"))]
{ std::time::SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() }
}┌─────────────────────────────────────────────────────────────────┐
│ Rust Forking Worker (WASM) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ rust-ofrep-worker-forking crate │ │
│ │ • OfrepHandler - HTTP request/response handling │ │
│ │ • OFREP types (Request, Response, Error) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ flagd-evaluator (js-time feature) │ │
│ │ • FlagEvaluator - standalone evaluation engine │ │
│ │ • Uses js_sys::Date::now() for timestamps │ │
│ │ • No OpenFeature SDK dependency │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Pure Rust Dependencies │ │
│ │ • datalogic-rs - JSONLogic evaluation │ │
│ │ • murmurhash3 - fractional rollout bucketing │ │
│ │ • serde_json - JSON parsing │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
- Standalone design: No OpenFeature SDK dependency, purpose-built for WASM
- Dual-runtime support: Same crate works in Chicory (Java) and Cloudflare Workers
- Native WASM: Compiles to WebAssembly bytecode, no
new Function()restrictions - Type safety: Full Rust type checking at compile time
- Same flag format: Uses identical flagd JSON configuration
For a detailed comparison of the two Rust approaches, see docs/rust-wasm-approaches.md.
Prerequisites:
- Rust toolchain via
rustup wasm32-unknown-unknowntarget:rustup target add wasm32-unknown-unknown- Node.js 25+ (for wrangler)
# Build the Rust forking worker
cd examples/rust-worker-forking
npx wrangler build
# Run locally
npx wrangler dev --port 8789use rust_ofrep_worker_forking::{OfrepHandler, OfrepRequest};
use worker::*;
const FLAGS_JSON: &str = include_str!("flags.json");
#[event(fetch)]
async fn main(mut req: Request, _env: Env, _ctx: Context) -> Result<Response> {
let handler = OfrepHandler::new(FLAGS_JSON)?;
let body: OfrepRequest = req.json().await.unwrap_or_default();
match handler.evaluate_flag("my-flag", &body) {
Ok(result) => Response::from_json(&result),
Err(error) => Response::from_json(&error),
}
}See packages/rust-ofrep-worker-forking/README.md for full documentation.
Both JS and Rust workers expose identical OFREP endpoints:
POST /ofrep/v1/evaluate/flags/{key}
Request:
{
"context": {
"targetingKey": "user-123",
"email": "user@example.com",
"plan": "premium"
}
}Response (200):
{
"key": "my-feature",
"value": true,
"reason": "TARGETING_MATCH",
"variant": "on",
"metadata": {
"flagSetId": "production"
}
}POST /ofrep/v1/evaluate/flags
Request:
{
"context": {
"targetingKey": "user-123"
}
}Response (200):
{
"flags": [
{
"key": "feature-a",
"value": true,
"reason": "STATIC",
"variant": "on"
}
],
"metadata": {
"flagSetId": "production",
"version": "1.0.0"
}
}# Health check
curl http://localhost:8787/
# Evaluate a single flag
curl -X POST http://localhost:8787/ofrep/v1/evaluate/flags/simple-boolean \
-H "Content-Type: application/json" \
-d '{"context": {"targetingKey": "user-123"}}'
# Evaluate with targeting context
curl -X POST http://localhost:8787/ofrep/v1/evaluate/flags/targeted-boolean \
-H "Content-Type: application/json" \
-d '{"context": {"targetingKey": "user-123", "email": "user@openfeature.dev"}}'
# Bulk evaluate all flags
curl -X POST http://localhost:8787/ofrep/v1/evaluate/flags \
-H "Content-Type: application/json" \
-d '{"context": {"targetingKey": "user-123"}}'Both implementations support all flagd targeting features:
| Feature | Description | Example |
|---|---|---|
| JSONLogic rules | Complex conditional logic | {"if": [{"==": [{"var": "plan"}, "premium"]}, "on", "off"]} |
| Fractional evaluation | Percentage-based rollouts | {"fractional": [["control", 50], ["treatment", 50]]} |
| String comparison | starts_with, ends_with |
{"starts_with": [{"var": "email"}, "admin"]} |
| Semantic versioning | Version comparison | {"sem_ver": [{"var": "version"}, ">=", "2.0.0"]} |
Flags use the flagd flag definition format. See examples/js-worker/src/flags.json for examples.
| Aspect | JavaScript Worker | Rust Worker | Rust Forking Worker |
|---|---|---|---|
| Bundle size (gzip) | ~180 KB | ~562 KB | ~667 KB |
| Evaluation engine | json-logic-engine (interpreted) | datalogic-rs (native) | datalogic-rs (native) |
| Build time | Fast (~2s) | Slower (~10s) | Slower (~10s) |
| Type safety | Runtime | Compile-time | Compile-time |
| Dependencies | Fork of flagd-core | Fork of rust-sdk-contrib | Fork of flagd-evaluator |
| Code generation | None (interpreter mode) | None (native WASM) | None (native WASM) |
| Upstream origin | OpenFeature JS SDK | OpenFeature Rust SDK | Forking flagd-evaluator |
All implementations pass the same OFREP compliance tests and support identical flag configurations.
- JavaScript Worker: Best for teams already using TypeScript/JavaScript, fastest build times
- Rust Worker: Best for teams using the OpenFeature Rust SDK ecosystem
- Rust Forking Worker: Best if you want a standalone evaluator with minimal dependencies, or are also using the Java/Chicory integration
- JavaScript Worker: Workers-compatible flagd-core fork
- Rust Worker: WASM-compatible flagd Rust SDK fork
- Rust Forking Worker: WASM-compatible forking-flagd-evaluator fork
- Upstream PRs: Contribute Workers/WASM compatibility back to upstream repos
- Performance Benchmarks: Compare JS vs Rust evaluation performance
- Cloudflare KV: Load flag configurations from KV at runtime
- Durable Objects: Real-time flag updates with WebSocket sync
- External Sync: Fetch flags from external HTTP endpoint
- Cache API: Cache evaluated results for performance
- ETag Support: Bulk evaluation caching with ETags
npm run buildnpm run dev:jscd examples/rust-worker
npx wrangler dev --port 8788cd examples/rust-worker-forking
npx wrangler dev --port 8789# JS SDK contrib
cd contrib/js-sdk-contrib
git pull origin feat/workers-compatibility
# Rust SDK contrib
cd contrib/rust-sdk-contrib
git pull origin feat/wasm-support
# flagd-evaluator (forking)
cd contrib/flagd-evaluator
git pull origin feat/js-time-featureIf the flagd JSON schemas change:
cd contrib/js-sdk-contrib/libs/shared/flagd-core
npm run build:validators- flagd - Feature flag evaluation engine
- OpenFeature - Open standard for feature flags
- OFREP Specification - OpenFeature Remote Evaluation Protocol
- js-sdk-contrib - OpenFeature JavaScript SDK contributions
- rust-sdk-contrib - OpenFeature Rust SDK contributions
- forking-flagd-evaluator - Standalone WASM-first flagd evaluator
These forks add Workers/WASM compatibility features that aren't yet upstream:
- DevCycleHQ-Sandbox/js-sdk-contrib -
feat/workers-compatibilitybranch - DevCycleHQ-Sandbox/rust-sdk-contrib -
feat/wasm-supportbranch - DevCycleHQ-Sandbox/flagd-evaluator -
feat/js-time-featurebranch
Apache-2.0