xiaohongshu
xiaohongshu
loop-breaker
Scaffold middleware to detect and block Worker self-recursion, infinite fetch chains, and runaway loops. Use this skill when designing Workers that call other Workers, implement webhooks, or handle events that might trigger themselves. Also use when encountering "recursion", "infinite loop", "sel...
Full skill instructions
Scaffold protective middleware to prevent infinite recursion, self-calling Workers, and runaway loops that cause "denial-of-wallet" attacks on your Cloudflare bill.
In serverless, a loop isn't just a frozen browser tab—it's a direct billing multiplier:
| Loop Type | Billing Impact | Example Cost |
|---|---|---|
| Worker → Worker fetch chain | Each hop = new Worker invocation | 1000 loops × $0.30/M = instant bill |
| D1 query in loop | Each iteration = row reads | 1M rows × $0.25/B = $0.25+ per loop |
| R2 write in loop | Each iteration = Class A op | 1000 writes × $4.50/M = $0.0045+ |
| DO kept alive | Duration billing continues | 1 hour idle = $0.045 |
The most common and costly loop occurs when a Worker calls itself (or calls a second Worker that calls the first) via fetch(). This creates an infinite chain of HTTP requests.
Core Implementation:
// src/middleware/recursion-guard.ts
import { createMiddleware } from 'hono/factory';
import type { Bindings } from '../types';
const RECURSION_HEADER = 'X-Recursion-Depth';
const DEFAULT_DEPTH_LIMIT = 3;
interface RecursionGuardOptions {
maxDepth?: number;
onLimitExceeded?: (depth: number) => Response;
}
export const recursionGuard = (options: RecursionGuardOptions = {}) => {
const maxDepth = options.maxDepth ?? DEFAULT_DEPTH_LIMIT;
return createMiddleware<{ Bindings: Bindings }>(async (c, next) => {
const currentDepth = parseInt(c.req.header(RECURSION_HEADER) || '0', 10);
if (currentDepth > maxDepth) {
console.error(`Recursion limit exceeded: depth=${currentDepth}, limit=${maxDepth}`);
if (options.onLimitExceeded) {
return options.onLimitExceeded(currentDepth);
}
// HTTP 508 Loop Detected (RFC 5842)
return c.json({
error: 'Loop Detected',
message: `Recursion depth ${currentDepth} exceeds limit ${maxDepth}`,
code: 'RECURSION_LIMIT_EXCEEDED'
}, 508);
}
// Store for use in outgoing requests
c.set('recursionDepth', currentDepth);
await next();
});
};
/**
* Create headers for outgoing fetch requests that preserve recursion tracking
*/
export function createRecursionHeaders(
currentHeaders: Headers,
currentDepth: number
): Headers {
const newHeaders = new Headers(currentHeaders);
newHeaders.set(RECURSION_HEADER, (currentDepth + 1).toString());
return newHeaders;
}
// src/index.ts
import { Hono } from 'hono';
import { recursionGuard, createRecursionHeaders } from './middleware/recursion-guard';
import type { Bindings } from './types';
// Extend Hono context to include recursionDepth
type Variables = {
recursionDepth: number;
};
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>();
// Apply recursion guard to all routes
app.use('*', recursionGuard({ maxDepth: 3 }));
// Example: Worker that calls another Worker
app.post('/webhook', async (c) => {
const currentDepth = c.get('recursionDepth');
// When making outgoing requests, pass the incremented depth
const response = await fetch('https://other-worker.example.com/process', {
method: 'POST',
headers: createRecursionHeaders(c.req.raw.headers, currentDepth),
body: await c.req.text(),
});
return c.json({ processed: true, depth: currentDepth });
});
export default app;
Add to src/types.ts:
// Add to Hono Variables
declare module 'hono' {
interface ContextVariableMap {
recursionDepth: number;
}
}
When using Service Bindings (RPC), you still need recursion protection but headers don't apply. Use a context-passing pattern:
// src/services/with-recursion-context.ts
interface RecursionContext {
depth: number;
maxDepth: number;
callChain: string[];
}
export function createRecursionContext(
workerName: string,
existingContext?: RecursionContext
): RecursionContext {
const ctx = existingContext ?? { depth: 0, maxDepth: 5, callChain: [] };
return {
depth: ctx.depth + 1,
maxDepth: ctx.maxDepth,
callChain: [...ctx.callChain, workerName],
};
}
export function checkRecursionLimit(ctx: RecursionContext): void {
if (ctx.depth > ctx.maxDepth) {
throw new RecursionLimitError(ctx);
}
// Detect cycles in call chain
const seen = new Set<string>();
for (const worker of ctx.callChain) {
if (seen.has(worker)) {
throw new RecursionCycleError(worker, ctx.callChain);
}
seen.add(worker);
}
}
class RecursionLimitError extends Error {
constructor(ctx: RecursionContext) {
super(`Recursion limit exceeded: ${ctx.depth} > ${ctx.maxDepth}`);
this.name = 'RecursionLimitError';
}
}
class RecursionCycleError extends Error {
constructor(worker: string, chain: string[]) {
super(`Recursion cycle detected: ${worker} in chain [${chain.join(' -> ')}]`);
this.name = 'RecursionCycleError';
}
}
// When calling another Worker via Service Binding
const ctx = createRecursionContext('api-gateway', incomingContext);
checkRecursionLimit(ctx);
const result = await c.env.DATA_SERVICE.fetchUser(userId, ctx);
Cloudflare Queues can create "Retry Loops" when a Worker fails to process a message due to a persistent bug. This burns CPU and D1 writes on every retry.
// src/middleware/idempotency-guard.ts
interface IdempotencyOptions {
kv: KVNamespace;
ttlSeconds?: number;
keyPrefix?: string;
}
export async function ensureIdempotent(
messageId: string,
options: IdempotencyOptions
): Promise<{ alreadyProcessed: boolean; markProcessed: () => Promise<void> }> {
const { kv, ttlSeconds = 86400, keyPrefix = 'processed:' } = options;
const key = `${keyPrefix}${messageId}`;
const existing = await kv.get(key);
if (existing) {
return {
alreadyProcessed: true,
markProcessed: async () => {},
};
}
return {
alreadyProcessed: false,
markProcessed: async () => {
await kv.put(key, Date.now().toString(), { expirationTtl: ttlSeconds });
},
};
}
// Queue consumer usage
export default {
async queue(batch: MessageBatch<QueueMessage>, env: Bindings): Promise<void> {
for (const msg of batch.messages) {
const { alreadyProcessed, markProcessed } = await ensureIdempotent(
msg.id,
{ kv: env.IDEMPOTENCY_KV }
);
if (alreadyProcessed) {
console.log(`Skipping duplicate message: ${msg.id}`);
msg.ack();
continue;
}
try {
await processMessage(msg.body, env);
await markProcessed();
msg.ack();
} catch (error) {
console.error(`Failed to process message ${msg.id}:`, error);
msg.retry(); // Will go to DLQ after max_retries
}
}
},
};
Durable Objects billing includes duration (wall time). A setInterval without clear termination keeps the DO active and billing.
// DO with safe interval handling
export class SafeTimerDO implements DurableObject {
private intervalId: ReturnType<typeof setInterval> | null = null;
private state: DurableObjectState;
constructor(state: DurableObjectState) {
this.state = state;
}
// BAD: This keeps DO alive forever
// startBadTimer() {
// setInterval(() => this.doWork(), 1000);
// }
// GOOD: Alarm-based timing (hibernation compatible)
async startSafeTimer(intervalMs: number): Promise<void> {
await this.state.storage.setAlarm(Date.now() + intervalMs);
}
async alarm(): Promise<void> {
await this.doWork();
// Reschedule only if still needed
const shouldContinue = await this.checkShouldContinue();
if (shouldContinue) {
await this.state.storage.setAlarm(Date.now() + 1000);
}
// Otherwise, DO hibernates and stops billing
}
private async checkShouldContinue(): Promise<boolean> {
// Check if there's a reason to keep running
const activeConnections = await this.state.storage.get('activeConnections');
return (activeConnections as number) > 0;
}
private async doWork(): Promise<void> {
// Periodic work here
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/start-timer') {
await this.startSafeTimer(1000);
return new Response('Timer started with hibernation support');
}
return new Response('OK');
}
}
Detect and prevent N+1 query patterns that multiply costs.
// src/utils/query-batcher.ts
/**
* Prevents N+1 queries by batching related lookups
*/
export class QueryBatcher<K, V> {
private pending: Map<K, { resolve: (v: V | null) => void; reject: (e: Error) => void }[]> = new Map();
private scheduled = false;
constructor(
private batchFn: (keys: K[]) => Promise<Map<K, V>>,
private maxBatchSize = 100,
private delayMs = 0
) {}
async load(key: K): Promise<V | null> {
return new Promise((resolve, reject) => {
const callbacks = this.pending.get(key) ?? [];
callbacks.push({ resolve, reject });
this.pending.set(key, callbacks);
if (!this.scheduled) {
this.scheduled = true;
if (this.delayMs > 0) {
setTimeout(() => this.dispatch(), this.delayMs);
} else {
queueMicrotask(() => this.dispatch());
}
}
});
}
private async dispatch(): Promise<void> {
const batch = new Map(this.pending);
this.pending.clear();
this.scheduled = false;
const keys = Array.from(batch.keys()).slice(0, this.maxBatchSize);
try {
const results = await this.batchFn(keys);
for (const [key, callbacks] of batch) {
const value = results.get(key) ?? null;
for (const { resolve } of callbacks) {
resolve(value);
}
}
} catch (error) {
for (const [, callbacks] of batch) {
for (const { reject } of callbacks) {
reject(error as Error);
}
}
}
}
}
// Usage example - prevents N+1 when fetching user data
const userBatcher = new QueryBatcher<string, User>(async (userIds) => {
const placeholders = userIds.map(() => '?').join(',');
const results = await db
.prepare(`SELECT * FROM users WHERE id IN (${placeholders})`)
.bind(...userIds)
.all<User>();
return new Map(results.results.map(u => [u.id, u]));
});
// Instead of: for (const id of userIds) { await getUser(id); } // N queries
// Use: await Promise.all(userIds.map(id => userBatcher.load(id))); // 1 query
{
"name": "loop-protected-worker",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
// Billing Safety: Kill runaway loops fast
"limits": {
"cpu_ms": 100
},
// Use Service Bindings instead of fetch() chains
"services": [
{ "binding": "AUTH_SERVICE", "service": "auth-worker" }
],
// Queue with DLQ to break retry loops
"queues": {
"consumers": [{
"queue": "events",
"max_retries": 1,
"dead_letter_queue": "events-dlq",
"max_concurrency": 10
}]
},
// KV for idempotency tracking
"kv_namespaces": [
{ "binding": "IDEMPOTENCY_KV", "id": "..." }
]
}
Use this checklist when reviewing code for loop vulnerabilities:
fetch( calls that might hit the same Workerwhile(true) or for(;;) without break conditionssetInterval in Durable Objects has termination logiclimits.cpu_ms set in wrangler config| Pattern | Risk | Solution |
|---|---|---|
fetch(request.url) | Infinite self-call | Use recursion depth header |
while (!success) retry | Unbounded retries | Add max attempts + backoff |
setInterval in DO | Billing never stops | Use alarm API |
D1 query in forEach | N+1 cost explosion | Use QueryBatcher |
| Webhook → same Worker | Recursive billing | Add depth limit |
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience