> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xquik.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limits

> Rate limit tiers, fixed window counter, and client-side rate limiting

<blockquote className="agent-llms-directive">
  For the complete documentation index, see <a href="/llms.txt">llms.txt</a>.
</blockquote>

Xquik enforces rate limits to ensure fair usage and platform stability. Limits are applied per user account using a fixed window counter.

<Note>
  **Quick answer:** 60 GET/1s, 30 POST/60s, 15 DELETE/60s per account.
  Hit the limit? Respect the `Retry-After` header. Avoid 429s? Use the client-side rate limiter below.
</Note>

## Rate limit tiers

<CardGroup cols={3}>
  <Card title="Read" icon="database">
    `GET`, `HEAD`, and `OPTIONS` share a limit of 60 requests per 1 second.
  </Card>

  <Card title="Write" icon="pen-line">
    `POST`, `PUT`, and `PATCH` share a limit of 30 requests per 60 seconds.
  </Card>

  <Card title="Delete" icon="circle-x">
    `DELETE` requests are limited to 15 requests per 60 seconds.
  </Card>
</CardGroup>

### Action-specific limits

Some write endpoints have stricter limits that apply on top of the general write tier:

<CardGroup cols={2}>
  <Card title="Connect account" icon="user">
    `POST /x/accounts` uses a high safety limit. If exceeded, wait for
    `Retry-After: 900` before retrying.
  </Card>

  <Card title="Follow" icon="gauge">
    `POST /x/users/{id}/follow` is limited to 20 requests per 60 seconds and 400 per day.
  </Card>
</CardGroup>

## How limits work

Xquik uses a **fixed window counter**:

<Steps>
  <Step title="Window starts on first request">
    The first request starts a 1-second read window or a 60-second write/delete window.
  </Step>

  <Step title="Each request increments the counter">
    Every request within the window increments the counter for its tier (read, write, or delete).
  </Step>

  <Step title="Counter resets after window expires">
    After that tier's window expires, the counter returns to zero.
  </Step>

  <Step title="Requests rejected when limit is reached">
    If the counter exceeds the tier limit within a window, subsequent requests return `429 Too Many Requests` until the window resets.
  </Step>
</Steps>

```text theme={null}
Read tier (60 per 1s):

Time 0.0s: [0/60]   -> Send 40 GET requests -> [40/60]
Time 0.5s: [40/60]  -> Send 20 GET requests -> [60/60] (limit reached)
Time 0.8s: [60/60]  -> GET request -> 429 (rejected, window active)
Time 1.0s: [0/60]   -> Window resets -> requests allowed again
```

## Response headers

When you exceed the rate limit, the response includes:

<CardGroup cols={2}>
  <Card title="Retry-After header" icon="timer">
    `Retry-After` is the wait time in seconds before retrying. Standard read throttles return `Retry-After: 1`. Write and delete throttles return `Retry-After: 60`. Account connection cooldowns return `Retry-After: 900`.
  </Card>

  <Card title="JSON retry field" icon="file-json">
    The JSON body includes `error: "rate_limit_exceeded"` and `retryAfter` in seconds, so clients can back off even when they do not expose response headers directly.
  </Card>
</CardGroup>

**Example 429 response:**

```text theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 1
Content-Type: application/json

{ "error": "rate_limit_exceeded", "message": "Too many requests. Try again later.", "retryAfter": 1 }
```

Always respect the `Retry-After` header. Requests sent before the fixed window resets keep returning `429` until `Retry-After` elapses.

## Client-side rate limiter

Prevent hitting server-side limits by implementing a client-side rate limiter. This is more efficient than relying on 429 responses and backoff.

<CodeGroup>
  ```javascript Node.js theme={null}
  class WindowRateLimiter {
    constructor(maxRequests, windowMs) {
      this.maxRequests = maxRequests;
      this.windowMs = windowMs;
      this.count = 0;
      this.windowStart = Date.now();
    }

    async acquire() {
      const now = Date.now();

      if (now - this.windowStart >= this.windowMs) {
        this.count = 0;
        this.windowStart = now;
      }

      if (this.count >= this.maxRequests) {
        const waitMs = this.windowMs - (now - this.windowStart);
        await new Promise((resolve) => setTimeout(resolve, waitMs));
        this.count = 0;
        this.windowStart = Date.now();
      }

      this.count += 1;
    }
  }

  // Read tier: 60 requests per 1s
  const readLimiter = new WindowRateLimiter(60, 1_000);

  async function apiRequest(url) {
    await readLimiter.acquire();
    return fetch(url, {
      headers: { "x-api-key": "xq_YOUR_KEY_HERE" },
    });
  }
  ```

  ```python Python theme={null}
  import time
  import threading

  class WindowRateLimiter:
      def __init__(self, max_requests: int, window_seconds: float):
          self.max_requests = max_requests
          self.window_seconds = window_seconds
          self.count = 0
          self.window_start = time.monotonic()
          self.lock = threading.Lock()

      def acquire(self):
          with self.lock:
              now = time.monotonic()

              if now - self.window_start >= self.window_seconds:
                  self.count = 0
                  self.window_start = now

              if self.count >= self.max_requests:
                  wait = self.window_seconds - (now - self.window_start)
                  time.sleep(wait)
                  self.count = 0
                  self.window_start = time.monotonic()

              self.count += 1

  # Read tier: 60 requests per 1s
  read_limiter = WindowRateLimiter(60, 1)

  def api_request(url: str) -> requests.Response:
      read_limiter.acquire()
      return requests.get(url, headers={"x-api-key": "xq_YOUR_KEY_HERE"})
  ```

  ```go Go theme={null}
  package main

  import (
  	"net/http"
  	"sync"
  	"time"
  )

  type WindowRateLimiter struct {
  	maxRequests int
  	window      time.Duration
  	count       int
  	windowStart time.Time
  	mu          sync.Mutex
  }

  func NewWindowRateLimiter(maxRequests int, window time.Duration) *WindowRateLimiter {
  	return &WindowRateLimiter{
  		maxRequests: maxRequests,
  		window:      window,
  		count:       0,
  		windowStart: time.Now(),
  	}
  }

  func (r *WindowRateLimiter) Acquire() {
  	r.mu.Lock()
  	defer r.mu.Unlock()

  	now := time.Now()

  	if now.Sub(r.windowStart) >= r.window {
  		r.count = 0
  		r.windowStart = now
  	}

  	if r.count >= r.maxRequests {
  		wait := r.window - now.Sub(r.windowStart)
  		r.mu.Unlock()
  		time.Sleep(wait)
  		r.mu.Lock()
  		r.count = 0
  		r.windowStart = time.Now()
  	}

  	r.count++
  }

  // Read tier: 60 requests per 1s
  var readLimiter = NewWindowRateLimiter(60, time.Second)

  func apiRequest(url string) (*http.Response, error) {
  	readLimiter.Acquire()
  	req, err := http.NewRequest("GET", url, nil)
  	if err != nil {
  		return nil, err
  	}
  	req.Header.Set("x-api-key", "xq_YOUR_KEY_HERE")
  	return http.DefaultClient.Do(req)
  }
  ```

  ```bash cURL theme={null}
  #!/bin/bash
  # Replay stored events in 100-item pages.
  # Read tier: 60 per 1s = ~17ms between requests.

  cursor=""

  while :; do
    url="https://xquik.com/api/v1/events?limit=100"

    if [ -n "$cursor" ]; then
      url="${url}&after=${cursor}"
    fi

    response=$(curl -s "$url" -H "x-api-key: xq_YOUR_KEY_HERE")
    echo "$response" | jq ".events[]"

    has_more=$(echo "$response" | jq -r ".hasMore")
    cursor=$(echo "$response" | jq -r ".nextCursor // empty")

    if [ "$has_more" != "true" ] || [ -z "$cursor" ]; then
      break
    fi

    sleep 0.02  # 20ms stays under 60 requests per second
  done
  ```
</CodeGroup>

## Rate limiting libraries

Instead of building your own rate limiter, consider these battle-tested libraries:

<CardGroup cols={3}>
  <Card title="Node.js libraries" icon="package">
    Use [bottleneck](https://github.com/SGrondin/bottleneck) with `npm install bottleneck`, or [p-limit](https://github.com/sindresorhus/p-limit) with `npm install p-limit`.
  </Card>

  <Card title="Python library" icon="package">
    Use [ratelimit](https://github.com/tomasbasham/ratelimit) with `pip install ratelimit`.
  </Card>

  <Card title="Go library" icon="package">
    Use [rate](https://pkg.go.dev/golang.org/x/time/rate) with `go get golang.org/x/time/rate`.
  </Card>
</CardGroup>

```javascript bottleneck example theme={null}
import Bottleneck from "bottleneck";

const limiter = new Bottleneck({
  reservoir: 60,            // 60 requests per read window
  reservoirRefreshAmount: 60,
  reservoirRefreshInterval: 1_000, // 1 second
  maxConcurrent: 5,
});

const response = await limiter.schedule(() =>
  fetch("https://xquik.com/api/v1/events?limit=50", {
    headers: { "x-api-key": "xq_YOUR_KEY_HERE" },
  })
);
```

## Best practices

<AccordionGroup>
  <Accordion title="Batch event replays with cursors">
    Fetch `GET /api/v1/events?limit=100`. If `hasMore` is `true`, store `nextCursor` and pass it as `after` on the next page. This uses 1 read slot per page instead of 1 request per monitor.
  </Accordion>

  <Accordion title="Use webhooks instead of polling">
    Active monitors already check every 1 second. Use signed webhooks for live delivery, then use `GET /api/v1/events?limit=100&after={nextCursor}` only for backfills and reconciliation. See the [webhooks overview](/webhooks/overview).
  </Accordion>

  <Accordion title="Cache responses client-side">
    Monitor and webhook configurations change infrequently. Cache `GET` responses for list endpoints and invalidate only after mutations (create, update, delete).
  </Accordion>

  <Accordion title="Implement exponential backoff">
    When you receive a 429, wait for the `Retry-After` duration. If the retry also fails, double the wait time. See the [error handling guide](/guides/error-handling) for complete retry implementations.
  </Accordion>

  <Accordion title="Spread requests evenly">
    Avoid sending all requests in a burst at the start of each window. Spread them evenly across each tier's window to avoid hitting the limit early.
  </Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Error Handling" icon="triangle-alert" href="/guides/error-handling">
    Retry strategies and error recovery patterns.
  </Card>

  <Card title="API Overview" icon="book" href="/api-reference/overview">
    Base URL, authentication, and conventions.
  </Card>
</CardGroup>
