{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-resources/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":["tabs","tab"]},"type":"markdown"},"seo":{"title":"Rate Limiting","description":"Bookable is a TMS API gateway API — one integration to access real-time availability and manage bookings across venues on any table management system.","llmstxt":{"hide":false,"sections":[{"title":"Table of contents","includeFiles":["**/*"],"excludeFiles":[]}],"excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"rate-limiting","__idx":0},"children":["Rate Limiting"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["All Bookable API endpoints are subject to rate limiting to ensure fair usage and consistent performance across all integration partners."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"overview","__idx":1},"children":["Overview"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Property"},"children":["Property"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Value"},"children":["Value"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Limit"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["200 requests per 60-second window"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Scope"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Per OAuth2 client (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sub"]}," claim)"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Exceeded response"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["HTTP 429 Too Many Requests"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Error code"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["RATE-R-001"]}]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Rate limits are applied ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["per OAuth2 client identity"]}," — identified by the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sub"]}," claim in your access token. This means your limit is shared across all servers, processes, and requests that authenticate using the same client credentials. If you run multiple workers or services under one client, their requests count toward a single shared quota."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"response-headers","__idx":2},"children":["Response headers"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Every API response includes headers so you can monitor your consumption in real time:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Header"},"children":["Header"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Present on"},"children":["Present on"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Description"},"children":["Description"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-RateLimit-Limit"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["All responses"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Total number of requests permitted in the current window (e.g. ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["200"]},")"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-RateLimit-Remaining"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["All responses"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Number of requests remaining before you are throttled (e.g. ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["150"]},")"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-RateLimit-Reset"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["All responses"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Unix timestamp (seconds since epoch) when the current window resets"]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"example-response-headers","__idx":3},"children":["Example response headers"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"http","header":{"controls":{"copy":{}}},"source":"HTTP/1.1 200 OK\nX-RateLimit-Limit: 200\nX-RateLimit-Remaining: 150\nX-RateLimit-Reset: 1741651200\n","lang":"http"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When the limit is exceeded:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"http","header":{"controls":{"copy":{}}},"source":"HTTP/1.1 429 Too Many Requests\nX-RateLimit-Limit: 200\nX-RateLimit-Remaining: 0\nX-RateLimit-Reset: 1741651200\nContent-Type: application/problem+json\n\n{\n  \"type\": \"https://tools.ietf.org/html/rfc6585#section-4\",\n  \"title\": \"Too Many Requests\",\n  \"status\": 429,\n  \"detail\": \"You have exceeded the rate limit of 200 requests per 60-second window.\",\n  \"code\": \"RATE-R-001\",\n  \"isRetryable\": true\n}\n","lang":"http"},"children":[]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"how-the-window-works","__idx":4},"children":["How the window works"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Bookable uses a ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["fixed 60-second window"]},". At the start of each window, your remaining quota is reset to the full limit (200). The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-RateLimit-Reset"]}," header on every response contains the Unix timestamp when the current window resets — subtract the current time to calculate how long to wait."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"Window 1 (0s–60s)       Window 2 (60s–120s)\n│◄──────────────────►│◄──────────────────►│\n│                    │                    │\n│  200 requests      │  Quota resets to   │\n│  consumed          │  200 again         │\n│  → 429 returned    │                    │\n"},"children":[]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"handling-rate-limit-errors","__idx":5},"children":["Handling rate limit errors"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When you receive a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["429"]},", read the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-RateLimit-Reset"]}," header and wait until that Unix timestamp before retrying. Retrying immediately will continue to return ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["429"]}," and waste time."]},{"$$mdtype":"Tag","name":"Tabs","attributes":{"size":"medium"},"children":[{"$$mdtype":"Tag","name":"div","attributes":{"label":"JavaScript / TypeScript","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"async function requestWithRetry(\n  url: string,\n  options: RequestInit,\n  maxRetries = 3\n): Promise<Response> {\n  for (let attempt = 0; attempt <= maxRetries; attempt++) {\n    const response = await fetch(url, options);\n\n    if (response.status !== 429) {\n      return response;\n    }\n\n    if (attempt === maxRetries) {\n      throw new Error(`Rate limit exceeded after ${maxRetries} retries`);\n    }\n\n    const resetAt = parseInt(response.headers.get(\"X-RateLimit-Reset\") ?? \"0\", 10);\n    const waitMs = Math.max((resetAt * 1000) - Date.now(), 1000);\n    console.warn(`Rate limited. Retrying in ${Math.ceil(waitMs / 1000)}s...`);\n    await new Promise((resolve) => setTimeout(resolve, waitMs));\n  }\n\n  throw new Error(\"Unreachable\");\n}\n","lang":"typescript"},"children":[]}]},{"$$mdtype":"Tag","name":"div","attributes":{"label":"Python","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"import time\nimport requests\n\ndef request_with_retry(url: str, headers: dict, max_retries: int = 3) -> requests.Response:\n    for attempt in range(max_retries + 1):\n        response = requests.get(url, headers=headers)\n\n        if response.status_code != 429:\n            return response\n\n        if attempt == max_retries:\n            raise Exception(f\"Rate limit exceeded after {max_retries} retries\")\n\n        reset_at = int(response.headers.get(\"X-RateLimit-Reset\", 0))\n        wait_seconds = max(reset_at - time.time(), 1)\n        print(f\"Rate limited. Retrying in {wait_seconds:.0f}s...\")\n        time.sleep(wait_seconds)\n\n    raise Exception(\"Unreachable\")\n","lang":"python"},"children":[]}]},{"$$mdtype":"Tag","name":"div","attributes":{"label":"Java","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"java","header":{"controls":{"copy":{}}},"source":"import java.net.http.*;\nimport java.time.Duration;\nimport java.time.Instant;\n\npublic HttpResponse<String> requestWithRetry(HttpClient client, HttpRequest request, int maxRetries)\n    throws Exception {\n\n    for (int attempt = 0; attempt <= maxRetries; attempt++) {\n        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n\n        if (response.statusCode() != 429) {\n            return response;\n        }\n\n        if (attempt == maxRetries) {\n            throw new RuntimeException(\"Rate limit exceeded after \" + maxRetries + \" retries\");\n        }\n\n        long resetAt = response.headers().firstValue(\"X-RateLimit-Reset\")\n            .map(Long::parseLong).orElse(0L);\n        long waitSeconds = Math.max(resetAt - Instant.now().getEpochSecond(), 1L);\n\n        System.out.println(\"Rate limited. Retrying in \" + waitSeconds + \"s...\");\n        Thread.sleep(Duration.ofSeconds(waitSeconds));\n    }\n\n    throw new IllegalStateException(\"Unreachable\");\n}\n","lang":"java"},"children":[]}]},{"$$mdtype":"Tag","name":"div","attributes":{"label":"C#","disable":false},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"csharp","header":{"controls":{"copy":{}}},"source":"using System.Net.Http;\n\npublic async Task<HttpResponseMessage> RequestWithRetryAsync(\n    HttpClient client, HttpRequestMessage request, int maxRetries = 3)\n{\n    for (int attempt = 0; attempt <= maxRetries; attempt++)\n    {\n        var response = await client.SendAsync(request);\n\n        if (response.StatusCode != System.Net.HttpStatusCode.TooManyRequests)\n            return response;\n\n        if (attempt == maxRetries)\n            throw new Exception($\"Rate limit exceeded after {maxRetries} retries\");\n\n        long resetAt = long.TryParse(\n            response.Headers.TryGetValues(\"X-RateLimit-Reset\", out var vals)\n                ? System.Linq.Enumerable.FirstOrDefault(vals) : null,\n            out var ts) ? ts : 0L;\n        var waitSeconds = Math.Max(resetAt - DateTimeOffset.UtcNow.ToUnixTimeSeconds(), 1L);\n        Console.WriteLine($\"Rate limited. Retrying in {waitSeconds}s...\");\n        await Task.Delay(TimeSpan.FromSeconds(waitSeconds));\n    }\n\n    throw new InvalidOperationException(\"Unreachable\");\n}\n","lang":"csharp"},"children":[]}]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"proactive-monitoring","__idx":6},"children":["Proactive monitoring"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Rather than waiting for a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["429"]},", inspect the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-RateLimit-Remaining"]}," header on every response to stay ahead of your limit."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"typescript","header":{"controls":{"copy":{}}},"source":"function checkRateLimit(response: Response): void {\n  const remaining = parseInt(response.headers.get(\"X-RateLimit-Remaining\") ?? \"200\", 10);\n  const limit = parseInt(response.headers.get(\"X-RateLimit-Limit\") ?? \"200\", 10);\n  const usagePercent = ((limit - remaining) / limit) * 100;\n\n  if (usagePercent >= 80) {\n    console.warn(`Rate limit at ${usagePercent.toFixed(0)}% — ${remaining} requests remaining`);\n  }\n}\n","lang":"typescript"},"children":[]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"best-practices","__idx":7},"children":["Best practices"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Batch where possible"]}," ","Use availability search with date ranges rather than issuing one request per date. Fewer, richer requests are more efficient than many narrow ones."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Partner-Reference"]}]}," ","Always set the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-Partner-Reference"]}," header on your requests. This makes it significantly easier to correlate your API calls with rate limit activity in support investigations."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Avoid thundering herd patterns"]}," ","If you have multiple workers, stagger their startup and avoid synchronized polling intervals. Workers hitting the API in lockstep can exhaust the shared window in a burst."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Cache aggressively"]}," ","Venue and product data changes infrequently. Cache responses locally for a few minutes rather than re-fetching on every user action."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Respect ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-RateLimit-Reset"]}," exactly"]}," ","Do not retry before the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-RateLimit-Reset"]}," timestamp has passed. Early retries return another ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["429"]}," and do not reset the clock."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"frequently-asked-questions","__idx":8},"children":["Frequently asked questions"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Does the limit apply per endpoint or globally?"]}," ","The 200 requests/60s limit is global across all API endpoints for your client. A mix of booking lookups and availability checks all count against the same quota."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Do failed requests (4xx, 5xx) count against my limit?"]}," ","Yes. All requests that reach the API — regardless of outcome — consume quota."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["What happens to requests in flight when the window resets?"]}," ","In-flight requests that arrived before the window reset are counted against the previous window. New requests after the reset begin consuming the refreshed quota."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Can my limit be increased?"]}," ","Contact your Bookable account team to discuss higher limits if your integration consistently approaches the default threshold."]}]},"headings":[{"value":"Rate Limiting","id":"rate-limiting","depth":1},{"value":"Overview","id":"overview","depth":2},{"value":"Response headers","id":"response-headers","depth":2},{"value":"Example response headers","id":"example-response-headers","depth":3},{"value":"How the window works","id":"how-the-window-works","depth":2},{"value":"Handling rate limit errors","id":"handling-rate-limit-errors","depth":2},{"value":"Proactive monitoring","id":"proactive-monitoring","depth":2},{"value":"Best practices","id":"best-practices","depth":2},{"value":"Frequently asked questions","id":"frequently-asked-questions","depth":2}],"frontmatter":{"seo":{"title":"Rate Limiting"}},"lastModified":"2026-03-10T11:02:55.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/resources/rate-limiting","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}