Skip to content

Protect /api/execute with cache, rate limit, and backend timeout - #19

Open
alexandramartinez wants to merge 2 commits into
bighnesh0007:masterfrom
alexandramartinez:fix/execute-backend-guards
Open

Protect /api/execute with cache, rate limit, and backend timeout#19
alexandramartinez wants to merge 2 commits into
bighnesh0007:masterfrom
alexandramartinez:fix/execute-backend-guards

Conversation

@alexandramartinez

Copy link
Copy Markdown
Collaborator

Why

DWCode went down under load. The bottleneck isn't the Next.js app or the data size — it's the DataWeave compiler backend (https://dwlbackend.onrender.com), a JVM that compiles + runs each script per request. /api/execute is the hot path that overwhelms a small/free instance, and it's amplified by Submit fanning out one backend call per test case (Workspace.tsx).

This PR adds three dependency-free, in-memory guards that cut load before it reaches the backend — zero new infra required.

What

New module lib/executeGuards.ts, wired into app/api/execute/route.ts. Guard state lives on globalThis so it survives warm invocations — the same pattern already used in lib/db.ts.

1. Result cache (biggest win)

DataWeave scripts are pure, so an identical {script, inputs} pair always yields the same output. On a practice site the same reference solutions get resubmitted constantly — those are served with no backend hit.

  • 5-min TTL, 1000-entry LRU cap (TTL only bounds memory; correctness doesn't need it)
  • Only successful runs are cached — never memoize a backend outage

2. Per-identity rate limit

30 runs/min keyed on Clerk userId, falling back to client IP for anonymous callers (the route is public). Over the limit → 429 + Retry-After. Stops one client, a stuck retry loop, or a bot from saturating the JVM.

3. Backend fetch timeout

The original fetch had no timeout — a hung compile would hold a connection indefinitely, and a pile-up of those turns a slow backend into a dead one. Added AbortSignal.timeout(15s) with a distinct "compiler overloaded, retry" message.

Verification

  • tsc --noEmit passes
  • lib/executeGuards.ts lints clean
  • The 3 pre-existing eslint any warnings on the route are unrelated to this change (confirmed via git stash)

Caveats

  • In-memory, per-instance. If DWCode later runs multiple backend instances, cache/counters are per-node — still strictly better than today. Move to Redis if global limits are ever needed; kept in-memory deliberately to ship today with no infra change.
  • Free-tier cold starts (JVM sleeping after idle) are a hosting decision, untouched here — but these guards mean a small always-on tier will comfortably absorb the load.

Possible follow-up

Batch the per-test-case Submit loop into a single backend request (cuts submit load by the average test-case count). The rate limiter in lib/executeGuards.ts is written to be reusable for that.

🤖 Generated with Claude Code

The DataWeave compiler backend is a JVM that compiles + runs each script,
so /api/execute is the hot path that overwhelms a small/free instance —
especially since Submit fans out one call per test case. Add three
dependency-free, in-memory guards that cut load before it reaches the backend:

- Result cache: identical {script, inputs} runs are pure, so memoize them
  (5 min TTL, 1000-entry LRU). High hit rate on a practice site where the
  same reference solutions get resubmitted constantly.
- Per-identity rate limit: 30 runs/min keyed on Clerk userId (falls back to
  IP for anonymous callers), returns 429 + Retry-After.
- Backend fetch timeout: abort after 15s so hung compiles can't pile up and
  wedge the whole instance.

State lives on globalThis (same pattern as lib/db.ts) so it survives warm
invocations. Only successful runs are cached — never a backend outage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

@alexandramartinez is attempting to deploy a commit to the bighnesh's projects Team on Vercel.

A member of the Team first needs to authorize it.

Submit used to fire 1 + N sequential fetches from the browser (custom-input
run + one per test case), each a full round-trip that also burned a
rate-limit token and opened a fresh backend connection. On a small instance
that fan-out is a big part of what tips the DataWeave compiler over.

- Extract the single-run core (validate → parse → cache → timeout backend
  call → normalize) into lib/dataweave.ts so both routes share identical
  behaviour and the SAME result cache.
- Add POST /api/execute/batch: { code, inputs[] } → { results[] }. Costs one
  rate-limit token per Submit (a batch is one user action, not N), caps batch
  size at 25, and runs inputs server-side through the shared cache. Cache hits
  (repeat solutions, identical test cases) return instantly.
- Slim /api/execute down to the shared core + its per-run rate limit.
- Rewire Workspace handleSubmit to make one batch call and map results back to
  test cases, preserving the existing pass/fail diff output.

Net: one Submit = one browser request + one rate-limit token, and repeat/
identical runs skip the backend entirely.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant