diff --git a/API.md b/API.md index c791223..87c60bc 100644 --- a/API.md +++ b/API.md @@ -1339,12 +1339,16 @@ Submit a blob directly to the batcher queue. Structure and cryptographic proofs **Rate-limited `429`** +Per client IP, `API_RATE_LIMIT_MAX` requests per minute across every `/v1/*` +route (default 600; `/v1/health` and `/v1/health/sync` are exempt). The +`x-ratelimit-limit`, `x-ratelimit-remaining` and `x-ratelimit-reset` headers +are set on every counted response; back off on `429` instead of retry-looping. + ```json { - "success": false, - "error": "Rate limit exceeded", - "message": "Too many requests. Please retry after 60 seconds.", - "retryAfter": 60 + "statusCode": 429, + "error": "RATE_LIMITED", + "reason": "Too many requests — please wait before retrying." } ``` diff --git a/FRONTEND-API-HANDOFF.md b/FRONTEND-API-HANDOFF.md index 76c6a97..a8dbbad 100644 --- a/FRONTEND-API-HANDOFF.md +++ b/FRONTEND-API-HANDOFF.md @@ -7,7 +7,7 @@ from memory. The backend will be **redeployed from zero** with these changes — is no compatibility window and no legacy fallback: the old API simply stops existing. Base URL: `http://:9999`. All offer-related endpoints moved from `/api/*` to -`/v1/*`. Rate limit: **60 requests/min per IP** across all routes → HTTP 429 +`/v1/*`. Rate limit: **600 requests/min per IP** (`API_RATE_LIMIT_MAX`) across all routes → HTTP 429 `{ "error": "RATE_LIMITED", "reason": "..." }`. On 429, back off; do not retry-loop. --- diff --git a/README.md b/README.md index 656b19b..6c87d47 100644 --- a/README.md +++ b/README.md @@ -437,6 +437,7 @@ Fund the `celestia1...` address shown by `celestia state account-address` with T | `CELESTIA_POLLING_INTERVAL_MS` | optional | Sync cadence. Defaults: devnet 6 000 ms, mainnet 30 000 ms. | | `MIDNIGHT_START_BLOCK` | yes | Numeric block height to start Midnight sync from. | | `NTP_START_TIME` | optional | NTP reference timestamp; resumed from DB when unset. | +| `API_RATE_LIMIT_MAX`, `API_RATE_LIMIT_ALLOWLIST` | optional | Per-client-IP budget for `/v1/*` per minute (default 600; `/v1/health*` exempt) and comma-separated IPs exempt from it. Size it for a solver plus its console on one address (~450/min worst case); `deploy/` sets 6000. | | `BATCHER_SUBMIT_TIMEOUT_MS` | optional | Absolute batcher fetch + receipt-body deadline; default 310 000 ms, bounded to 1 000–600 000 ms. | | `API_SSE_MAX_CONNECTIONS` | optional | Per-node concurrent `/v1/offers/stream` cap; default 100. Excess clients receive `503 SSE_CAPACITY`. | | `API_UPDATES_MAX_CONNECTIONS` | optional | Per-node concurrent `/v1/offers/updates` websocket cap; default 100. Excess clients are refused the connection (this endpoint's refusals are disconnects, not HTTP statuses — see API.md). | diff --git a/deploy/.env.example b/deploy/.env.example index d73a6c6..4bb4617 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -214,7 +214,7 @@ SOLVER_FRONTEND_POLL_MS= SOLVER_FRONTEND_HISTORY_LIMIT= # ── kernel ─────────────────────────────────────────────────────────────────── -# The dev per-IP budget (60/min) throttles a co-located solver during a +# The code default per-IP budget (600/min) still throttles a co-located solver during a # page-through plus settlement polls, and would throttle the E2E driver itself. API_RATE_LIMIT_MAX=6000 API_RATE_LIMIT_ALLOWLIST= diff --git a/packages/node/api.ts b/packages/node/api.ts index 28c98ec..6f05295 100644 --- a/packages/node/api.ts +++ b/packages/node/api.ts @@ -101,7 +101,7 @@ export const apiRouter: StartConfigApiRouter = async function ( server: any, dbConn: any, ): Promise { - // Per-IP request budget (default 60/min) — applied to every route in this + // Per-IP request budget (default 600/min) — applied to every route in this // router. // // `statusCode` is load-bearing, not decoration: @fastify/rate-limit THROWS @@ -745,7 +745,7 @@ export const apiRouter: StartConfigApiRouter = async function ( // Uses effectstream.effectstream_blocks for NTP and // effectstream.sync_protocol_pagination for parallel chains. // Chain tips are fetched from the Midnight indexer / Celestia RPC and cached 60 s. - // Exempt from the 60/min API budget — UIs poll this as a liveness probe. + // Exempt from the per-IP API budget — UIs poll this as a liveness probe. server.get("/v1/health/sync", { config: { rateLimit: false } }, async () => { return getSyncStatus(dbConn); }); diff --git a/packages/node/env.ts b/packages/node/env.ts index 76e5de3..5099305 100644 --- a/packages/node/env.ts +++ b/packages/node/env.ts @@ -129,7 +129,7 @@ export const OFFER_MAX_BYTES = parseInt( // building a router gets them — the same reason isTokenRegistryEnabled below // is a function. export const apiRateLimitMax = (): number => - parseInt(getEnv("API_RATE_LIMIT_MAX") ?? "60"); + parseInt(getEnv("API_RATE_LIMIT_MAX") ?? "600"); export const apiRateLimitAllowList = (): string[] => (getEnv("API_RATE_LIMIT_ALLOWLIST") ?? "")