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

# API Rate Limits

> Per-app sliding-window rate limits, headers, and best practices

# API Rate Limits

The LaunchMyStore scoped REST API (`/api/v1/`) is rate-limited per
app + store using a sliding window. Limits are
**per-second** and **tier-based** — the app's tier on the developer
dashboard controls the cap.

<Note>
  Rate limits apply only to the **OAuth-scoped REST API** under
  `/api/v1/`. Internal admin endpoints, public storefront routes, and
  the platform proxy routes (`/api/apps/*`) are not subject to this
  guard.
</Note>

## Limits by tier

| Tier         | Requests / second | Use case                                               |
| ------------ | ----------------- | ------------------------------------------------------ |
| `free`       | **20**            | Hobby/test apps, internal tools.                       |
| `basic`      | **40**            | Default tier for new published apps.                   |
| `pro`        | **100**           | Production apps with batch sync or background workers. |
| `enterprise` | **500**           | High-volume integrations, ERP/data-warehouse syncs.    |

Tier is set per-app on the developer dashboard. See
[App Types & Tiers](/getting-started/app-types-and-tiers#app-tiers).

### Algorithm

A sliding 1-second window per `(app, store)` pair:

1. Every request's timestamp is recorded for the pair.
2. On each request, entries older than 1 second are evicted and the
   remaining count is checked against the tier limit.
3. If `count >= limit`, the request is rejected with **HTTP 429**.
   Otherwise the timestamp is added and the request proceeds.

This means the limit is a true rolling rate, not a fixed-bucket reset —
a burst of N requests blocks further calls until enough of those
timestamps age past one second.

## Response headers

Every response from `/api/v1/` includes these headers, whether the
request succeeded or was rate-limited:

| Header                  | Description                                                                |
| ----------------------- | -------------------------------------------------------------------------- |
| `X-RateLimit-Limit`     | Maximum requests allowed per window for this app's tier.                   |
| `X-RateLimit-Remaining` | Approximate remaining requests in the current window. Floored at `0`.      |
| `X-RateLimit-Reset`     | Unix timestamp (seconds) when the current window resets.                   |
| `Retry-After`           | **Only on 429.** Seconds to wait before retrying — equals the window size. |

Example:

```http theme={null}
HTTP/1.1 200 OK
X-RateLimit-Limit: 40
X-RateLimit-Remaining: 37
X-RateLimit-Reset: 1747402805
Content-Type: application/json

{ "data": { ... } }
```

## 429 response

When the cap is exceeded:

```http theme={null}
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 40
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1747402805
Retry-After: 1
Content-Type: application/json

{
  "error": "Too Many Requests",
  "message": "Rate limit exceeded. Retry after 1 second(s)."
}
```

<Warning>
  A 429 means *this app*'s requests on *this store* exceeded the cap.
  Other stores using the same app are unaffected. Other apps on the
  same store are unaffected.
</Warning>

## Best practices

### Exponential backoff on 429

Always honor `Retry-After` and add a small random jitter to spread
retries across multiple instances of your worker.

<CodeGroup>
  ```javascript Node.js theme={null}
  async function callWithRetry(fn, maxRetries = 5) {
    for (let attempt = 0; attempt < maxRetries; attempt++) {
      const res = await fn();
      if (res.status !== 429) return res;

      const retryAfter = parseInt(res.headers.get('retry-after') || '1', 10);
      const baseDelay = retryAfter * 1000;
      // Exponential backoff with jitter
      const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 250;
      await new Promise((r) => setTimeout(r, delay));
    }
    throw new Error('Rate limit exhausted after retries');
  }
  ```

  ```python Python theme={null}
  import asyncio, random

  async def call_with_retry(fn, max_retries=5):
      for attempt in range(max_retries):
          res = await fn()
          if res.status_code != 429:
              return res
          retry_after = int(res.headers.get('retry-after', '1'))
          base_delay = retry_after
          delay = base_delay * (2 ** attempt) + random.uniform(0, 0.25)
          await asyncio.sleep(delay)
      raise Exception('Rate limit exhausted after retries')
  ```

  ```ruby Ruby theme={null}
  def call_with_retry(max_retries: 5)
    max_retries.times do |attempt|
      res = yield
      return res unless res.code.to_i == 429
      retry_after = (res['retry-after'] || '1').to_i
      delay = retry_after * (2 ** attempt) + rand * 0.25
      sleep(delay)
    end
    raise 'Rate limit exhausted after retries'
  end
  ```
</CodeGroup>

### Read the headers proactively

Don't wait for a 429 — check `X-RateLimit-Remaining` after every
response and slow down when it drops below a safety threshold.

```javascript theme={null}
function shouldThrottle(res) {
  const remaining = parseInt(res.headers.get('x-ratelimit-remaining') || '40', 10);
  const limit = parseInt(res.headers.get('x-ratelimit-limit') || '40', 10);
  // Slow down when under 20% remaining
  return remaining < limit * 0.2;
}

if (shouldThrottle(res)) {
  await new Promise((r) => setTimeout(r, 250));
}
```

### Batch requests where the API supports it

Several REST resources expose bulk endpoints to amortize the cap:

* **Metafields**: `POST /metafields/bulk` upserts up to 250 metafields
  in one call. See [Metafields](/api-reference/metafields).
* **Products**: pagination via `?page=…&limit=…` returns up to 250
  products per call.
* **Orders**: bulk fetch with `?ids=id1,id2,id3,…`.

One batched call costs one slot vs. N individual calls — usually the
single biggest win for a sync-style app.

### Use webhooks instead of polling

Many sync workflows poll the API every few seconds to detect changes.
Subscribe to a [webhook topic](/api-reference/webhooks/topics) instead
— LaunchMyStore pushes the event to your endpoint as it happens,
costing zero rate-limit slots on your side.

### Use background workers, not on-request fan-out

If a single end-user action triggers ten outbound API calls, those ten
calls all count toward the per-second cap in the same window.
Enqueueing them into a background worker that paces itself smooths
the load.

### Avoid the leader-election trap

Two instances of your app both polling on a one-second schedule will
double the request rate. Use a Redis lock or a single scheduler to
ensure only one instance fires the polled call per interval.

## Multi-store apps

The rate-limit key includes both `appId` and `storeId`. A single app
installed across 1000 stores has its own per-store window for each —
the limits don't pool. So an app at the `basic` tier with 1000
installs can sustain `40 × 1000 = 40,000 req/sec` system-wide, as long
as the requests are spread across stores.

## Configuring tier on the developer dashboard

App tier is set on the developer dashboard's app settings page. Tier
upgrades are handled by:

1. The merchant accepting a higher subscription tier for the app, or
2. The developer requesting a tier upgrade via support (for first-party
   apps that don't go through the marketplace flow).

Tier lookups are cached in-process for 60 seconds — a tier change can
take up to 60s to propagate.

## Failures and fallbacks

<AccordionGroup>
  <Accordion title="Can rate limits be temporarily raised?">
    Not for individual apps via API. Tier-level limits apply
    uniformly. If your integration has a one-off catch-up backfill
    requirement, contact support — for some integrations a temporary
    `enterprise` tier can be granted.
  </Accordion>

  <Accordion title="Are GET and POST counted differently?">
    No. Every authenticated request to `/api/v1/` regardless of method
    counts as one slot.
  </Accordion>

  <Accordion title="Do 4xx responses count?">
    Yes — every request that reaches the guard counts toward the
    window, even if it ultimately 4xx's on validation. Avoid
    "ping-and-error" patterns.
  </Accordion>
</AccordionGroup>

## See also

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/api-reference/authentication">
    OAuth flow, scopes, and Bearer-token format.
  </Card>

  <Card title="App Types & Tiers" icon="layer-group" href="/getting-started/app-types-and-tiers">
    What each tier unlocks beyond rate limits.
  </Card>

  <Card title="Webhooks Overview" icon="bell" href="/api-reference/webhooks/overview">
    Push notifications — zero rate-limit cost on your side.
  </Card>

  <Card title="Function Active Limits" icon="bolt" href="/functions/active-limits">
    A parallel per-shop cap for declarative functions.
  </Card>
</CardGroup>
