Skip to content

Edge example

Cloudflare Workers’ fetch handlers are stateless — request → response, no long-lived state, no UI. There’s no reactive work for Para to do there, and forcing it would just be ceremony. Where Para earns its keep on the edge is the Durable Object: a single instance per IP that lives across many requests, holding state Para’s signals are built for.

This page shows that Durable Object — the actual rate limiter logic. The fetch handler that calls into it is plain Workers code; a one-liner version is at the bottom for completeness, but it isn’t where the Para syntax lives.

export class RateLimiter {
signal count = 0;
signal windowStart = Date.now();
derived overLimit = count > 100;
constructor(state) {
this.state = state;
when overLimit {
console.log(`rate limit crossed: ${count} requests in window`);
}
}
async fetch(req) {
if (Date.now() - windowStart > 60_000) {
count = 0;
windowStart = Date.now();
}
count++;
return Response.json({
allowed: !overLimit,
remaining: Math.max(0, 100 - count),
});
}
}
  • signal count + signal windowStart — mutable cells holding per-IP state across many requests. Each request increments count; once a minute the window resets.
  • derived overLimit = count > 100 — recomputes whenever count changes. Read-only; written using the derived keyword to make the intent explicit (writing signal here would auto-promote to the same shape, but derived says “you can’t assign to this” at the declaration site).
  • when overLimit { ... } — fires once per false→true transition. Logs the first request that crosses the threshold; doesn’t re-fire on every subsequent over-limit request until count drops back below 100 and rises again. This is the edge-triggered shape that’s clumsy without language support — the alternative is hand-tracking the previous value or an unconditional effect that does its own diffing.

The fetch method is plain JS — no Para sugar. That’s fine: the per-call work isn’t reactive, the cross-call state is. Putting the right tool at the right layer is the point.

The fetch handler that proxies requests through the Durable Object is plain Workers code; nothing reactive happens here, so it’s plain JavaScript:

export default {
async fetch(req, env) {
const ip = req.headers.get("cf-connecting-ip") ?? "unknown";
const stub = env.RATE_LIMIT.get(env.RATE_LIMIT.idFromName(ip));
const { allowed, remaining } = await (await stub.fetch("https://internal/check")).json();
if (!allowed) {
return new Response("rate limited", {
status: 429,
headers: { "Cache-Control": "public, max-age=10", "Retry-After": "60" },
});
}
return Response.json({ ok: true, remaining }, {
headers: { "X-RateLimit-Remaining": String(remaining) },
});
},
};
{
"name": "my-worker",
"main": "dist/index.js",
"compatibility_date": "2026-04-30",
"durable_objects": {
"bindings": [{ "name": "RATE_LIMIT", "class_name": "RateLimiter" }]
},
"migrations": [{ "tag": "v1", "new_classes": ["RateLimiter"] }]
}
{
"name": "my-worker",
"type": "module",
"scripts": {
"build": "parabun build src/index.pjs --target browser --outfile dist/index.js",
"deploy": "parabun run build && wrangler deploy"
},
"dependencies": { "@para/signals": "*" },
"devDependencies": { "wrangler": "^4" }
}

--target browser is correct for Workers — the V8 isolate runtime has web-platform globals but no Node APIs.

Terminal window
parabun install
parabun run deploy

The output is standard JavaScript using import "@para/signals". Wrangler bundles it like any other npm-using Worker.

If you prefer types, swap src/limiter.pjs for src/limiter.pts, declare the Env interface (RATE_LIMIT: DurableObjectNamespace), and type state: DurableObjectState on the class. Same compiled output either way.