Skip to content

Rate limiting

Handbid rate-limits every V3 endpoint. The intent is to protect the platform from runaway clients and abusive bots — not to nickel-and-dime legitimate integrations. If you’re hitting a limit in production traffic you didn’t expect, talk to us; budgets are tunable per integration.

Each call is keyed on the endpoint and on who is making it:

  • Authenticated calls key on userId. Two users on the same network don’t collide.
  • Anonymous calls key on the caller’s IP. An unkeyable anonymous request (no resolvable IP) is treated as adversarial and gets a 429 immediately.

The window is a fixed-second counter — typically 60 seconds. After the window rolls over the count resets. There’s no leaky-bucket smoothing.

When you exceed a budget:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
Access-Control-Expose-Headers: Retry-After
{
"error": "rate_limited",
"message": "Rate limit exceeded. Retry after 60 seconds.",
"retryAfter": 60
}

Retry-After is exposed via CORS so browser clients can read it. retryAfter in the body is the same value — included for convenience on clients that find header introspection awkward.

Honor the value. Don’t retry sooner. If you do, you’re just adding to the count for nothing.

These are the V3 production defaults. They’re tuned per-environment via params.v3RateLimits; QA may be more permissive.

EndpointAuthedAnonymous
GET /v3/auctions60/min30/min
GET /v3/auctions/{id}60/min30/min
GET /v3/auction/{id}/items60/min30/min
GET /v3/item/{id}120/min60/min
GET /v3/item/{id}/bids60/min30/min
EndpointAuthedNotes
GET /v3/bidder/my-auctions30/min
GET /v3/bidder/invited-auctions20/min
GET /v3/bidder/my-activity60/min
GET /v3/bidder/my-cart30/min
GET /v3/bidder/favorites30/min
POST/DELETE /v3/bidder/favorite/{itemId}60/minShared budget across the two verbs
GET /v3/bidder/notifications120/minHigh to cover badge polling
PUT /v3/bidder/notifications/read-all20/minOne per screen-open is expected
POST /v3/item/{itemId}/bid60/min
POST /v3/item/{itemId}/auto-bid30/min
DELETE /v3/item/{itemId}/auto-bid30/min
EndpointAnonymousNotes
POST /v3/auth/email-check15/minLower with captcha-missing fallback
POST /v3/auth/register10/minLower with captcha-missing fallback

/v3/auth/email-check and /v3/auth/register have a second throttle layered underneath, defending the SMS-cost attack surface:

  • Per IP per endpoint
  • 10 requests per 5 minutes when a captcha token is missing or invalid
  • Retry-After: 300 on the over-budget response

If you’re integrating server-to-server (not browser), the strict throttle will hit you within a few requests. Either get a Handbid-scoped reCAPTCHA site key so you can send valid tokens, or have us add an integration-level exemption. Email engineering@handbid.com.

A few principles:

  • High-frequency reads get high budgets. The notifications endpoint’s limit=0 mode is a badge poll — clients can call it every few seconds on app foreground. 120/min is the ceiling.
  • One-per-screen-open writes get low budgets. Bulk read-all happens exactly once when the user opens the notifications screen; 20/min is more than a frantic user can produce.
  • Discovery is moderate. Browsing isn’t a tight loop, but a misbehaving client could thrash it; 60/min authed covers normal pagination.
  • Auth is the most expensive to scale. Each call may trigger SMS delivery (real dollars). Budgets are deliberately tight.

Things that will land you in 429 jail:

  • Polling without backoff. If you saw a 429, wait retryAfter. Don’t burn through your budget faster after a rejection.
  • Re-fetching the full list when only the count changed. Use limit=0 count-only mode for badge updates; only request the rows when the user navigates to the screen.
  • Generating tokens for every request. Mint once, cache for expiresIn seconds, reuse.
  • Calling discovery endpoints with browser-style rapid pagination on a server. Servers should batch + cache, not act like a frantic UI.

The budgets are configured per-environment in params.v3RateLimits. If your integration legitimately needs more throughput on a specific endpoint:

  1. Tell us which endpoint and what your expected steady-state rate is.
  2. We can either raise the global budget (if the use case is broad) or add an integration-specific budget keyed on your token.
  3. Allow a few days for the rollout — the change ships in the next release.

Cache-friendly patterns generally work better than higher budgets. Most “need more headroom” requests turn into “you don’t actually need to refresh this every second” conversations.

The throttle is implemented in V3RestController::enforceRateLimit() (HAN-1867). The auth-flow strict throttle is in V3RestController::enforceStrictRateLimit() (HAN-1576). Both are exercised by testing/API_tests/tests/V3/han-1867-rate-limiting.spec.ts.