Skip to main content
The rate-limiter-worker/ package is a standalone Cloudflare Worker that provides IP-based rate limiting via a SQLite-backed Durable Object. It must be deployed before the main Astro app.

Architecture

The main Astro Pages app cannot export Durable Object classes — Cloudflare Pages Functions have that restriction. The rate limiter is therefore hosted in a separate Worker and bound to the main app via a Durable Object RPC binding.

SlidingWindowRateLimiter

The SlidingWindowRateLimiter class extends DurableObject and uses SQLite-backed storage (available on all plans). Each unique IP gets its own Durable Object instance — Cloudflare routes requests to the geographically nearest instance automatically.

Rate limit parameters

How it works

The Durable Object maintains a requests table in SQLite:
On each checkLimit call:
  1. Expired entries (outside the sliding window) are pruned with a single DELETE
  2. Current window count is fetched with COUNT(*)
  3. If count ≥ maxRequests: returns { allowed: false, remaining: 0, retryAfterMs }
  4. Otherwise: inserts the current timestamp and returns { allowed: true, remaining: N }
A Durable Object alarm is scheduled to prune the table after the window expires, allowing the DO to hibernate automatically when idle.

Response interface

wrangler.jsonc configuration

The migrations block with new_sqlite_classes registers SlidingWindowRateLimiter as an SQLite-backed Durable Object. This migration runs once on first deploy.

Main app bindings

The main app (astro-app/wrangler.jsonc) references the rate limiter via a cross-script Durable Object binding:
The script_name field tells the Pages Worker to find the Durable Object class in the deployed rate-limiter-worker script.

D1 database (PORTAL_DB)

The main app is bound to a D1 database for the sponsor portal:
Access pattern in SSR routes:
Use db.batch() to combine multiple D1 queries into a single round-trip. Each batch() call counts as one subrequest against the 50/request free-plan limit (1,000 on paid plan).

KV store (SESSION_CACHE)

A KV namespace is bound for session caching:
The middleware degrades gracefully if SESSION_CACHE is unavailable — sessions fall back to re-validating the JWT on every request.

Deploy the rate limiter

1

Authenticate with Cloudflare

2

Deploy the worker

From the monorepo root:
Or from the rate-limiter-worker/ directory directly:
The first deploy runs the v1 migration and creates the SlidingWindowRateLimiter SQLite-backed Durable Object class.
3

Verify deployment

4

Deploy the main app

With the rate limiter deployed, the main Astro app can now resolve the RATE_LIMITER binding. Deploy or redeploy the ywcc-capstone Pages project.
Deploy the rate-limiter-worker before the main Astro app. If the RATE_LIMITER binding cannot be resolved, the Pages Worker will fail at startup.

Local development

For local dev, the main app’s wrangler.jsonc references the rate limiter DO binding. Run Wrangler dev in the rate-limiter-worker/ directory first, then run wrangler pages dev in astro-app/:
Alternatively, local dev bypasses auth via the import.meta.env.DEV check in middleware.ts, so the rate limiter is not exercised in standard astro dev.

Testing

The rate limiter worker has its own Vitest test suite using @cloudflare/vitest-pool-workers:

Observability

The rate-limiter-worker has observability: { enabled: true } in wrangler.jsonc. Metrics (requests, CPU time, errors) are visible in the Cloudflare Dashboard → Workers & Pages → rate-limiter-worker → Metrics tab.