npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@littlebearapps/platform-consumer-sdk

v3.1.0

Published

Platform Consumer SDK — automatic metric collection, circuit breaking, cost protection, and agent provisioning for Cloudflare Workers

Readme

Platform Consumer SDK

@littlebearapps/platform-consumer-sdk — Automatic cost protection, circuit breaking, and telemetry for Cloudflare Workers.

Install in each Worker project. Zero production dependencies. Ships raw TypeScript (bundled by wrangler).

Install

npm install @littlebearapps/platform-consumer-sdk

Using Claude Code? Install the Platform SDK Plugin for automated SDK convention enforcement — it validates wrangler bindings, budget wrappers, and cost safety patterns in real time.

tsconfig Requirement

The SDK ships raw .ts source files that wrangler bundles at deploy time. Your tsconfig.json must use:

{
  "compilerOptions": {
    "moduleResolution": "bundler"
  }
}

This is the default for new wrangler projects. If you see TypeScript errors after installing, check this setting first.

Quick Start

Wrap fetch handlers

import { withFeatureBudget, completeTracking, CircuitBreakerError } from '@littlebearapps/platform-consumer-sdk';

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const tracked = withFeatureBudget(env, 'myapp:api:main', { ctx });
    try {
      const result = await tracked.DB.prepare('SELECT * FROM users LIMIT 100').all();
      return Response.json(result);
    } catch (e) {
      if (e instanceof CircuitBreakerError) {
        return Response.json({ error: 'Feature temporarily disabled' }, { status: 503 });
      }
      throw e;
    } finally {
      ctx.waitUntil(completeTracking(tracked));
    }
  }
};

Wrap cron handlers

import { withCronBudget, completeTracking } from '@littlebearapps/platform-consumer-sdk';

export default {
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    const tracked = withCronBudget(env, 'myapp:cron:daily-sync', { ctx, cronExpression: event.cron });
    try {
      // Your cron logic using tracked.DB, tracked.KV, etc.
    } finally {
      ctx.waitUntil(completeTracking(tracked));
    }
  }
};

Wrap queue handlers

import { withQueueBudget, completeTracking } from '@littlebearapps/platform-consumer-sdk';

export default {
  async queue(batch: MessageBatch, env: Env, ctx: ExecutionContext) {
    for (const msg of batch.messages) {
      const tracked = withQueueBudget(env, 'myapp:queue:processor', {
        message: msg.body, queueName: 'my-queue',
      });
      try {
        // Process using tracked.DB, tracked.KV, etc.
        msg.ack();
      } finally {
        ctx.waitUntil(completeTracking(tracked));
      }
    }
  }
};

Required Bindings

Add to your wrangler.jsonc:

{
  "kv_namespaces": [
    { "binding": "PLATFORM_CACHE", "id": "YOUR_KV_NAMESPACE_ID" }
  ],
  "queues": {
    "producers": [
      { "binding": "TELEMETRY_QUEUE", "queue": "your-telemetry-queue" }
    ]
  }
}

PLATFORM_CACHE stores circuit breaker state and budget configuration. TELEMETRY_QUEUE carries metrics to the Platform backend.

How It Works

When you call withFeatureBudget(env, featureId, options), the SDK wraps your environment object with a three-layer proxy stack:

Layer 1 — Metrics Proxy

Every binding on env (DB, KV, AI, R2, QUEUE, etc.) is wrapped with a type-specific proxy that intercepts method calls and increments counters in a per-request MetricsAccumulator. For example, tracked.DB.prepare(sql).all() increments d1Reads and records d1RowsRead from the query metadata.

What gets tracked automatically:

| Binding | Metrics Tracked | |---------|----------------| | D1 | d1Reads, d1Writes, d1RowsRead, d1RowsWritten (per-statement via meta) | | KV | kvReads (get/getWithMetadata), kvWrites (put), kvDeletes, kvLists | | R2 | r2ClassA (put/delete/list/multipart), r2ClassB (get/head) | | Workers AI | aiRequests, model breakdown via aiModelCounts | | Vectorize | vectorizeQueries (query/getByIds), vectorizeInserts (insert/upsert) | | Queue | queueMessages (send/sendBatch) | | Durable Objects | doRequests, latency tracking (avg/max/p99) | | Workflow | workflowInvocations (create/createBatch) |

Layer 2 — Circuit Breaker Proxy

Wraps the metrics proxy. On the first access to a non-Platform binding (lazy/deferred — not at withFeatureBudget call time), the SDK checks three KV keys in parallel:

  1. CONFIG:GLOBAL:STATUS — Global kill switch
  2. CONFIG:PROJECT:{project}:STATUS — Project-level stop
  3. CONFIG:FEATURE:{featureId}:STATUS — Feature-level stop (set by budget enforcement)

If any returns STOP, a CircuitBreakerError is thrown. The check result is cached for the remainder of the request.

Synchronous builder methods (prepare, bind, get, idFromName, idFromString, fetch, connect) are bound to the original target rather than async-wrapped. This means CircuitBreakerError is thrown by the subsequent async method (e.g. .all(), .run()), not by the builder itself. This also prevents "Illegal invocation" errors on native Cloudflare Fetcher bindings.

Layer 3 — Health/Fetch Proxy

Adds two methods to the tracked env:

  • tracked.health() — Dual-plane health check (KV connectivity + queue delivery test)
  • tracked.fetch(url, init) — Standard fetch that auto-detects AI Gateway URLs and tracks usage

Telemetry Flush

When you call completeTracking(tracked), the SDK:

  1. Reads the MetricsAccumulator from the tracked env
  2. Builds a TelemetryMessage (skips if all metrics are zero and no errors)
  3. Sends to TELEMETRY_QUEUE via queue.send()fails open (logs but never throws)
  4. Clears the tracking context

Important: completeTracking must be called on the object returned by withFeatureBudget, not on the original env. The tracking context is stored in a WeakMap keyed on the proxy object.

Feature ID Convention

Feature IDs follow the format project:category:feature — three colon-separated parts:

| Example | Project | Category | Feature | |---------|---------|----------|---------| | scout:ocr:process | scout | ocr | process | | brand-copilot:scanner:github | brand-copilot | scanner | github | | myapp:api:main | myapp | api | main | | myapp:cron:daily-sync | myapp | cron | daily-sync | | myapp:queue:processor | myapp | queue | processor |

Conventions:

  • Use kebab-case for all parts
  • project should match your services.yaml project key
  • category groups features by billing/operational concern: api, cron, queue, scanner, connector, ai, do
  • feature identifies the specific operation
  • Each unique feature ID is a separate budget-trackable unit in budgets.yaml

See Feature ID Guide for detailed conventions and budget registration.

Exports

Main (@littlebearapps/platform-consumer-sdk)

Core Wrappers

| Export | Description | |--------|------------| | withFeatureBudget(env, featureId, opts?) | Wrap fetch handlers — proxies bindings with automatic tracking | | withCronBudget(env, featureId, opts) | Wrap scheduled handlers (deterministic correlation ID from cron expression) | | withQueueBudget(env, featureId, opts?) | Wrap queue handlers (extracts correlation ID from message body) | | completeTracking(env) | Flush pending metrics — call in finally or ctx.waitUntil() | | CircuitBreakerError | Thrown when a feature's budget is exhausted (has featureId, level, reason) | | health(featureId, kv, queue?, ctx?) | Dual-plane health check (KV connectivity + queue delivery) | | RequestBudgetExceededError | Thrown when per-invocation requestLimits exceeded (has metric, currentValue, limit) | | RecursionDepthError | Thrown when X-Recursion-Depth exceeds maxRecursionDepth | | SlidingWindowExceededError | Error class for cross-invocation rate limit breaches |

Defence in Depth (Phase 3)

| Export | Description | |--------|------------| | detectAnomalies(metrics, thresholds, cost?) | Stateless anomaly detection (zero I/O) | | checkSlidingWindowRateLimit(kv, count, metric, key, limit, windowMs?) | KV two-bucket sliding window rate check | | recordWindowCount(kv, key, metric, count, windowMs?) | Fire-and-forget window count increment | | checkD1StorageGuard(db, kv, config?) | D1 PRAGMA-based storage monitoring with KV cache | | createLimitedMetricsAccumulator(limits) | Proxy accumulator that throws on limit breach | | estimateCostFromMetrics(metrics) | Per-invocation USD cost estimate (raw CF pricing) | | forecastMonthlyCost(input) | Monthly cost projection with allowance analysis | | METRIC_TO_LIMIT | Mapping from MetricsAccumulator keys to RequestLimits keys |

Computation Modules (v1.4.0)

| Export | Description | |--------|------------| | addSample(state, value) | Reservoir sampling — add value using Algorithm R | | getPercentiles(state) | Compute p50/p75/p90/p95/p99 from reservoir samples | | createReservoirState() | Create fresh reservoir (100 samples, ~800 bytes) | | mergeReservoirs(a, b) | Combine reservoirs from multiple sources | | getReservoirState(featureId, kv) | Load reservoir from KV | | saveReservoirState(featureId, state, kv) | Save reservoir to KV (24h TTL) | | computePID(state, input, config?) | PID controller — compute throttle rate (0.0–1.0) | | createPIDState() | Create fresh PID state (zero integral/error) | | getPIDState(featureId, kv) | Load PID state from KV | | savePIDState(featureId, state, kv) | Save PID state + throttle rate to KV | | getThrottleRate(featureId, kv) | Get current throttle rate for SDK consumption | | calculateUtilisation(usage, limit) | Usage/limit ratio (can exceed 1.0) | | calculateBCU(metrics, weights?) | Budget Consumption Units from FeatureMetrics | | checkBCUBudget(currentBCU, limitBCU) | Check BCU budget state | | usdToBCU(usd) / bcuToUSD(bcu) | Convert between USD and BCU | | formatBCUResult(result) | Human-readable BCU summary | | calculateBillingPeriod(cycleDay, date?) | Billing period boundaries (calendar or mid-month) | | prorateAllowance(monthly, days, billingDays?) | Pro-rate monthly allowance to query period | | calculateBillableUsage(usage, allowance, days) | Billable usage after allowance subtraction | | getBillingWindow(anchorDay, date?) | ISO date strings for SQL queries | | calculateCFCostFromMetrics(metrics) | USD cost from FeatureMetrics (queue telemetry) |

Circuit Breaker Management

| Export | Description | |--------|------------| | isFeatureEnabled(featureId, kv) | Check if a feature is enabled (returns boolean) | | setCircuitBreakerStatus(featureId, status, kv, reason?) | Set GO/STOP status for a feature | | clearCircuitBreakerCache() | Clear per-request circuit breaker cache |

Logging and Correlation

| Export | Description | |--------|------------| | createLogger(opts) | Structured JSON logger with correlation IDs | | createLoggerFromEnv(env, worker, featureId?) | Logger auto-configured from environment | | createLoggerFromRequest(request, env, worker, featureId?) | Logger from incoming request headers (x-correlation-id, cf-ray) | | generateCorrelationId() | Generate a new UUID correlation ID | | getCorrelationId(env) / setCorrelationId(env, id) | Get/set correlation ID on environment |

Error Tracking

| Export | Description | |--------|------------| | categoriseError(error) | Categorise as VALIDATION, NETWORK, AUTH, RATE_LIMIT, D1_ERROR, etc. | | reportError(env, error) | Report error to telemetry context (increments errorCount) | | reportErrorExplicit(env, category, code?) | Report with explicit category and code | | withErrorTracking(env, fn) | Wrapper that automatically reports errors on catch (re-throws) | | trackError(metrics, error) | Track error count directly on a metrics accumulator | | hasErrors(env) / getErrorCount(env) | Query error state from context |

Distributed Tracing (W3C Traceparent)

| Export | Description | |--------|------------| | createTraceContext(request, env) | Extract or create trace context, stores on env | | extractTraceContext(request) | Parse incoming traceparent header, create new span ID | | getTraceContext(env) / setTraceContext(env, ctx) | Get/set trace context on environment | | parseTraceparent(header) / formatTraceparent(ctx) | Parse/format W3C 00-{traceId}-{spanId}-{flags} | | propagateTraceContext(ctx) | Create child span headers for outgoing requests | | createTracedFetch(ctx) | Wrapped fetch that auto-propagates trace context | | startSpan(ctx, name) / endSpan(span) / failSpan(span, error) | Span lifecycle management |

Timeout Utilities

| Export | Description | |--------|------------| | withTimeout(fn, timeoutMs?, operation?) | Race an async function against a timeout (default 30s) | | withTrackedTimeout(env, fn, timeoutMs, operation) | Timeout with automatic error reporting to telemetry | | withRequestTimeout(handler, timeoutMs, operation) | Wrap an entire Worker fetch handler with a 504 timeout | | timeoutResponse(error?) | Generate a 504 Gateway Timeout response | | TimeoutError | Error class with operation, timeoutMs, actualMs properties | | DEFAULT_TIMEOUTS | Presets: short (5s), medium (15s), long (30s), max (60s) |

Service Client (Cross-Worker Correlation)

| Export | Description | |--------|------------| | createServiceClient(env, sourceService, opts?) | Service binding wrapper with correlation + trace propagation | | wrapServiceBinding(fetcher, env, sourceService) | Lightweight Fetcher wrapper that merges context headers | | createServiceBindingHeaders(env, sourceService) | Generate correlation + trace headers for manual use | | extractCorrelationChain(request) | Extract { correlationId, sourceService, traceId, ... } from incoming request |

AI Gateway

| Export | Description | |--------|------------| | createAIGatewayFetch(env) | Drop-in fetch replacement that auto-detects AI Gateway URLs | | createAIGatewayFetchWithBodyParsing(env) | Same but also parses request body for model name (more accurate) | | parseAIGatewayUrl(url) | Extract { provider, model, accountId, gatewayId } from AI Gateway URL | | reportAIGatewayUsage(env, provider, model) | Track AI call metrics manually |

Supported providers: google-ai-studio, openai, deepseek, anthropic, workers-ai, azure-openai, bedrock, groq, mistral, perplexity.

Proxy Utilities

| Export | Description | |--------|------------| | createD1Proxy(db, metrics) | D1 binding proxy with metrics | | createKVProxy(kv, metrics) | KV binding proxy with metrics | | createAIProxy(ai, metrics) | Workers AI proxy with metrics | | createR2Proxy(r2, metrics) | R2 binding proxy with metrics | | createQueueProxy(queue, metrics) | Queue binding proxy with metrics | | createVectorizeProxy(index, metrics) | Vectorize binding proxy with metrics | | createDOProxy(ns, metrics) | Durable Object namespace proxy with latency tracking | | createWorkflowProxy(workflow, metrics) | Workflow binding proxy with metrics | | createEnvProxy(env, metrics) | Top-level env proxy (auto-detects binding types) | | getMetrics(env) | Extract accumulated metrics from a tracked env |

Other Utilities

| Export | Description | |--------|------------| | pingHeartbeat(ctx, url, token, success?) | Non-blocking Gatus/uptime heartbeat ping | | withExponentialBackoff(fn, attempts?) | Retry with exponential backoff (default 3 attempts, 100ms base) | | withHeartbeat(DOClass, config) | Durable Object mixin with alarm-based heartbeats | | PRICING_TIERS / PAID_ALLOWANCES | Cloudflare pricing constants (Workers Paid plan) | | calculateHourlyCosts(metrics) | Hourly cost breakdown with prorated allowances | | calculateDailyBillableCosts(usage, daysElapsed, daysInPeriod) | Daily billable costs with partial-month proration | | KV_KEYS | Key generation functions: featureStatus(id), projectStatus(id), globalStatus(), etc. | | CIRCUIT_STATUS | { GO: 'GO', STOP: 'STOP' } | | METRIC_FIELDS | 20-element Analytics Engine field mapping (positions locked) | | BINDING_NAMES | { PLATFORM_CACHE, PLATFORM_TELEMETRY } | | PLATFORM_FEATURES / getAllPlatformFeatures() | Pre-defined feature IDs for Platform workers |

Sub-path Exports

@littlebearapps/platform-consumer-sdk/middleware

Project-level circuit breaker middleware. Two-tier system: feature-level (SDK core, GO/STOP) + project-level (this module, active/warning/paused).

import { createCircuitBreakerMiddleware, CB_PROJECT_KEYS } from '@littlebearapps/platform-consumer-sdk/middleware';

// Hono middleware
const app = new Hono<{ Bindings: Env }>();
app.use('*', createCircuitBreakerMiddleware(CB_PROJECT_KEYS.SCOUT, {
  skipPaths: ['/health', '/healthz'],
}));

| Export | Description | |--------|------------| | checkProjectCircuitBreaker(key, kv) | Simple check — returns 503 Response or null | | checkProjectCircuitBreakerDetailed(key, kv) | Detailed check with status, reason, and response | | createCircuitBreakerMiddleware(key, opts?) | Hono-compatible middleware factory | | getCircuitBreakerStates(kv, keys?) | Query multiple projects at once | | getProjectStatus(kv, key) / setProjectStatus(kv, key, status, ttl?) | Read/write project CB state | | isGlobalStopActive(kv) / setGlobalStop(kv, enabled) | Global kill switch | | createProjectKey(slug) | Generate PROJECT:{SLUG}:STATUS key | | CB_PROJECT_KEYS | Pre-defined keys for Scout, Brand Copilot, Platform, etc. | | PROJECT_CB_STATUS | Status values: active, warning, paused |

See Middleware Guide for detailed usage.

@littlebearapps/platform-consumer-sdk/patterns

125 static regex patterns for classifying transient (expected operational) errors. Zero I/O — pure in-memory matching.

import { classifyErrorAsTransient } from '@littlebearapps/platform-consumer-sdk/patterns';

const result = classifyErrorAsTransient('quotaExceeded: Daily limit reached');
// { isTransient: true, category: 'quota-exhausted' }

| Export | Description | |--------|------------| | TRANSIENT_ERROR_PATTERNS | Array of 125 patterns with regex + category | | classifyErrorAsTransient(message) | Classify a message — returns { isTransient, category? } |

Categories include: quota-exhausted, rate-limited, service-unavailable, timeout, connection-refused, connection-timeout, dns-not-found, d1-rate-limited, do-reset, r2-internal-error, and many more.

@littlebearapps/platform-consumer-sdk/dynamic-patterns

AI-discovered patterns loaded from KV at runtime. Constrained DSL with ReDoS protection.

import { loadDynamicPatterns, classifyWithDynamicPatterns } from '@littlebearapps/platform-consumer-sdk/dynamic-patterns';
import type { CompiledPattern } from '@littlebearapps/platform-consumer-sdk/dynamic-patterns';

// Defensive wrap recommended in critical paths (e.g., tail handlers)
let patterns: CompiledPattern[] = [];
try {
  patterns = await loadDynamicPatterns(env.PLATFORM_CACHE);
} catch (e) {
  console.error('Failed to load dynamic patterns:', e);
}
const result = classifyWithDynamicPatterns('Custom error message', patterns);

| Export | Description | |--------|------------| | loadDynamicPatterns(kv) | Load approved patterns from KV (5-minute in-memory cache) | | compileDynamicPatterns(rules) | Compile and validate pattern rules | | classifyWithDynamicPatterns(message, patterns) | Classify against loaded patterns | | exportDynamicPatterns(kv) | Export patterns JSON for cross-account sync | | importDynamicPatterns(kv, json) | Import with validation gate (compiles as safety check) | | clearDynamicPatternsCache() | Clear in-memory cache (for testing or post-update) |

DSL types: contains (all tokens must appear), startsWith, statusCode (word-boundary match), regex (200-char limit).

@littlebearapps/platform-consumer-sdk/heartbeat

import { pingHeartbeat } from '@littlebearapps/platform-consumer-sdk/heartbeat';

// Non-blocking — uses ctx.waitUntil internally
pingHeartbeat(ctx, 'https://status.example.com/api/v1/endpoints/heartbeats_myworker/external', token);

@littlebearapps/platform-consumer-sdk/retry

import { withExponentialBackoff } from '@littlebearapps/platform-consumer-sdk/retry';

// 3 attempts: immediate, 100ms, 200ms (max 1s backoff)
const result = await withExponentialBackoff(() => fetchExternalAPI(url));

@littlebearapps/platform-consumer-sdk/costs

Cloudflare pricing tiers and cost calculation helpers. Updated for current Workers Paid plan pricing.

import { calculateHourlyCosts, PRICING_TIERS, PAID_ALLOWANCES } from '@littlebearapps/platform-consumer-sdk/costs';

const costs = calculateHourlyCosts(hourlyMetrics);
// { workers, d1, kv, r2, durableObjects, vectorize, aiGateway, workersAI, pages, queues, workflows, total }

@littlebearapps/platform-consumer-sdk/telemetry-sampling

O(1) memory reservoir sampling for latency percentile tracking (Algorithm R).

import { createReservoirState, addSample, getPercentiles } from '@littlebearapps/platform-consumer-sdk/telemetry-sampling';

const state = createReservoirState(); // 100 samples, ~800 bytes
addSample(state, responseTimeMs);
const p = getPercentiles(state);
// p.p50, p.p95, p.p99, p.avg, p.max

@littlebearapps/platform-consumer-sdk/control

PID controller for smooth throttle rate calculation (0.0–1.0) instead of binary ON/OFF.

import { getPIDState, computePID, savePIDState, calculateUtilisation } from '@littlebearapps/platform-consumer-sdk/control';

const state = await getPIDState(featureId, kv);
const usage = calculateUtilisation(currentUsage, budgetLimit);
const output = computePID(state, { currentUsage: usage, deltaTimeMs: 60000 });
await savePIDState(featureId, output.newState, kv);
// output.throttleRate: 0.0 (no throttle) to 1.0 (full throttle)

@littlebearapps/platform-consumer-sdk/economics

Scarcity-weighted quota enforcement via Budget Consumption Units (BCU).

import { calculateBCU, checkBCUBudget, formatBCUResult } from '@littlebearapps/platform-consumer-sdk/economics';

const result = calculateBCU(featureMetrics);
const budget = checkBCUBudget(result.total, 50000);
console.log(formatBCUResult(result)); // "BCU: 12345.67, dominant: AI compute (neurons) (89.2%)"

@littlebearapps/platform-consumer-sdk/billing

Billing period utilities with calendar-month and mid-month cycle support.

import { calculateBillingPeriod, getBillingWindow, prorateAllowance } from '@littlebearapps/platform-consumer-sdk/billing';

const period = calculateBillingPeriod(15); // Mid-month billing (day 15)
const window = getBillingWindow(1); // Calendar month → { startDate: '2026-03-01', endDate: '2026-03-31' }
const dailyAllowance = prorateAllowance(10_000_000, 1, period.daysInPeriod); // ~333K

Phase 3: Defence in Depth

v1.3.0 adds three layers of cost protection that work alongside existing circuit breakers.

Per-Invocation Request Limits

Hard-stop runaway handlers mid-request:

const tracked = withFeatureBudget(env, 'myapp:api:bulk-import', {
  ctx,
  requestLimits: { d1Writes: 500, kvWrites: 100 },
});

try {
  for (const item of items) {
    await tracked.DB.prepare('INSERT INTO items ...').bind(item.id).run();
    // Throws RequestBudgetExceededError if d1Writes exceeds 500
  }
} catch (e) {
  if (e instanceof RequestBudgetExceededError) {
    console.error(`Stopped at ${e.metric}: ${e.currentValue}/${e.limit}`);
  }
}

Anti-Loop Guard

Prevent worker-to-worker infinite recursion:

const tracked = withFeatureBudget(env, 'myapp:api:main', {
  ctx,
  incomingRequest: request,  // Reads X-Recursion-Depth header
  maxRecursionDepth: 5,      // Default: 5
});

// tracked.fetch() auto-increments X-Recursion-Depth
// Throws RecursionDepthError if depth exceeded

Anomaly Detection (Observational)

Flag unusual usage in telemetry without blocking:

const tracked = withFeatureBudget(env, 'myapp:cron:sync', {
  ctx,
  anomalyThresholds: {
    d1Writes: { warning: 1000, critical: 5000 },
    aiRequests: { warning: 50 },
    estimatedCostUsd: { warning: 0.10, critical: 0.50 },
  },
});

Anomalies appear in the TelemetryMessage as anomalies: AnomalyResult[].

Sliding Window Rate Limits

Catch sustained abuse across invocations (e.g., a cron running 200 D1 writes every minute):

const tracked = withFeatureBudget(env, 'myapp:cron:sync', {
  ctx,
  slidingWindowLimits: {
    d1Writes: { limit: 10000, windowMs: 3_600_000 }, // 10K/hour
  },
});

D1 Storage Guard

Monitor database size via PRAGMA queries:

const tracked = withFeatureBudget(env, 'myapp:api:main', {
  ctx,
  d1StorageGuard: {
    warningPct: 80,
    criticalPct: 95,
    maxBytes: 10_737_418_240, // 10 GB
  },
});

Real-Time Cost Estimation

Check costs during long-running operations:

const tracked = withFeatureBudget(env, 'myapp:api:bulk', { ctx });

for (const batch of batches) {
  await processBatch(tracked, batch);
  const cost = tracked.estimatedCost();
  if (cost.total > 0.50) {
    break; // Abort before costs accumulate
  }
}

Error Handling

CircuitBreakerError is thrown when any level of the circuit breaker hierarchy is STOP:

try {
  const tracked = withFeatureBudget(env, 'myapp:api:main', { ctx });
  await tracked.DB.prepare('SELECT 1').all(); // CB check happens here (first binding access)
} catch (e) {
  if (e instanceof CircuitBreakerError) {
    console.log(e.featureId); // 'myapp:api:main'
    console.log(e.level);     // 'feature' | 'project' | 'global'
    console.log(e.reason);    // Optional reason string
    return new Response('Service unavailable', { status: 503 });
  }
}

The hierarchy checks in order: global (kill switch) > project > feature. If any level returns STOP, the error includes which level triggered it.

Error Collection

Projects using the SDK automatically get error collection via the error-collector tail worker. The tail worker monitors all worker invocations and creates GitHub issues for failures — no application code changes needed.

What Gets Captured

| Cloudflare Outcome | Error Type | Priority | Notes | |---|---|---|---| | exception | exception | P0–P2 (tier-based) | Uncaught exceptions | | exceededCpu | cpu_limit | P0 | CPU time limit exceeded | | exceededMemory | memory_limit | P0 | Memory limit exceeded | | canceled | canceled | P2 | Workflow timeout, step failure | | responseStreamDisconnected | stream_disconnected | P3 | Client disconnected during streaming | | scriptNotFound | script_not_found | P0 | Deployment issue — script missing | | ok + console.error | soft_error | P2–P3 | Application-level errors | | ok + console.warn | warning | P4 (daily digest) | Warnings aggregated into daily digests |

Cloudflare Workflows: canceled events are common when Workflow steps timeout or are killed by the runtime. Workflows emit tail events under the parent worker's script name with eventType: 'rpc' and entrypoint showing the Workflow class.

See Error Collection Setup for full configuration details.

Configuration

Budget limits and circuit breaker thresholds are stored in KV (PLATFORM_CACHE) under the CONFIG:FEATURE: prefix. They're synced from budgets.yaml via the Admin SDK's sync script.

Requires a Platform backend — scaffold one with @littlebearapps/platform-admin-sdk.

Updating

npm update @littlebearapps/platform-consumer-sdk

The Consumer SDK is a pure library with zero side effects on update. No database migrations, no KV writes, no config changes. New features are additive and backward compatible within the same major version.

SDKOptions Reference

Options accepted by withFeatureBudget:

| Option | Type | Default | Description | |--------|------|---------|-------------| | ctx | ExecutionContext | — | Enables waitUntil for non-blocking telemetry flush | | checkCircuitBreaker | boolean | true | Whether to check KV for STOP status | | reportTelemetry | boolean | true | Whether to queue metrics on completion | | cacheKv | KVNamespace | env.PLATFORM_CACHE | Override KV namespace for CB checks | | telemetryQueue | Queue | env.PLATFORM_TELEMETRY | Override telemetry queue | | correlationId | string | auto-generated UUID | Request tracing ID | | externalCostUsd | number | 0 | Add external API costs (OpenAI, Apify, etc.) | | requestLimits | RequestLimits | — | Per-invocation operation caps (throws RequestBudgetExceededError) | | maxRecursionDepth | number | 5 | Max X-Recursion-Depth before RecursionDepthError | | incomingRequest | Request | — | Extract X-Recursion-Depth from incoming request | | anomalyThresholds | AnomalyThresholds | — | Flag anomalous usage in telemetry (observational) | | slidingWindowLimits | SlidingWindowLimits | — | Cross-invocation KV-backed rate limits | | tripCircuitBreakerOnWindowExceeded | boolean | false | Trip circuit breaker when sliding window exceeded | | d1StorageGuard | D1StorageGuardOptions | — | Monitor D1 size via PRAGMA queries |

Troubleshooting

ReferenceError: PLATFORM_CACHE is not defined Add the KV binding to your wrangler.jsonc. See Required Bindings.

Metrics not appearing in Platform dashboard Check that TELEMETRY_QUEUE is bound and the queue exists. The SDK never throws if the queue binding is missing — telemetry is silently dropped. Verify with wrangler queues list.

CircuitBreakerError on every request A circuit breaker is stuck in STOP state. Check KV:

wrangler kv key get CONFIG:FEATURE:myapp:api:main:STATUS --namespace-id YOUR_KV_ID

Reset with:

wrangler kv key put CONFIG:FEATURE:myapp:api:main:STATUS GO --namespace-id YOUR_KV_ID

Feature usage not tracked Verify the feature ID is registered in budgets.yaml and you've run npm run sync:config to push it to KV.

Illegal invocation when accessing service bindings This was fixed in v1.0.0. The SDK now correctly binds native Fetcher methods (fetch, connect) rather than wrapping them in async proxies. Upgrade to v1.0.0+.

completeTracking seems to have no effect completeTracking must be called on the object returned by withFeatureBudget, not the original env. The tracking context is stored in a WeakMap keyed on the proxy object:

// Correct
const tracked = withFeatureBudget(env, 'myapp:api:main', { ctx });
await completeTracking(tracked);

// Wrong — env has no tracking context
await completeTracking(env);

TypeScript errors after installing Ensure "moduleResolution": "bundler" in your tsconfig.json. The SDK ships raw .ts source, not compiled .js + .d.ts. Also ensure @cloudflare/workers-types is installed as a dev dependency.

Cannot find module '@littlebearapps/platform-consumer-sdk/middleware' Sub-path exports require "moduleResolution": "bundler" or "nodenext". The "node" resolution strategy does not support exports map in package.json.

See Troubleshooting Guide for more issues and solutions.

Multi-Account Support

The Consumer SDK works in any Cloudflare account — each account just needs its own KV namespace and telemetry queue. The SDK code is identical across accounts; only the wrangler binding IDs differ.

See the Multi-Account Setup guide for architecture patterns and detailed instructions.

Further Reading

Licence

MIT — Made with ❤️ by Little Bear Apps 🐶