# Rate limits and performance

This page is for developers whose integrations make many calls, such as nightly loads, bulk enrollment, or data extracts. It states the rate limits, explains how to stay under them, and shows how to move more data in fewer calls.

The examples on this page only read data.

## The limits

VBAPI allows **25 requests per second sustained, with bursts of up to 50, per API key**. A request over the limit is rejected with `429 Too Many Requests`.

"Per API key" means every caller that uses the same key shares one allowance: every service, scheduled job, script, and worker thread, and anyone using "Try it" in the API reference with that key. If three workers share a key, together they still get 25 requests per second, so give each about a third. If one integration needs to be isolated from another, ask VBA whether it should have its own key.

## Stay under the limit

Two things keep an integration healthy:

1. **Pace your own calls.** A client-side limiter set a little below the limit avoids most `429` responses.
2. **Back off when you do get a `429`.** The shared client retries `429` with exponential backoff and jitter; see [Errors and troubleshooting](/developer-portal/concepts/errors#retry-only-what-can-succeed-later).


This token-bucket limiter allows a steady rate with short bursts. It is set to 20 requests per second to leave room for other callers on the same key:

C#
```csharp
// Token bucket: allows `rate` calls per second on average and bursts of up to `burst` calls.
public sealed class ConceptsRateLimiter
{
    readonly double _rate;
    readonly double _burst;
    readonly SemaphoreSlim _lock = new(1, 1);
    readonly Stopwatch _clock = Stopwatch.StartNew();
    double _tokens;
    double _updated;

    public ConceptsRateLimiter(double rate = 20, int burst = 20)
    {
        _rate = rate;
        _burst = burst;
        _tokens = burst;
    }

    // Waits until a call is allowed. Safe to share between tasks.
    public async Task AcquireAsync()
    {
        while (true)
        {
            TimeSpan wait;
            await _lock.WaitAsync();
            try
            {
                var now = _clock.Elapsed.TotalSeconds;
                _tokens = Math.Min(_burst, _tokens + (now - _updated) * _rate);
                _updated = now;
                if (_tokens >= 1)
                {
                    _tokens -= 1;
                    return;
                }
                wait = TimeSpan.FromSeconds((1 - _tokens) / _rate);
            }
            finally
            {
                _lock.Release();
            }
            await Task.Delay(wait);
        }
    }
}
```

Python
```python
class RateLimiter:
    """Token bucket: allows `rate` calls per second on average and bursts of up to `burst` calls."""

    def __init__(self, rate=20.0, burst=20):
        self.rate = rate
        self.burst = burst
        self.tokens = float(burst)
        self.updated = time.monotonic()
        self.lock = threading.Lock()

    def acquire(self):
        """Block until a call is allowed. Thread-safe."""
        while True:
            with self.lock:
                now = time.monotonic()
                self.tokens = min(self.burst, self.tokens + (now - self.updated) * self.rate)
                self.updated = now
                if self.tokens >= 1:
                    self.tokens -= 1
                    return
                wait = (1 - self.tokens) / self.rate
            time.sleep(wait)
```

TypeScript
```typescript
/** Token bucket: allows `rate` calls per second on average and bursts of up to `burst` calls. */
export class RateLimiter {
  private tokens: number;
  private updated = performance.now();
  private queue: Promise<void> = Promise.resolve();

  constructor(private rate = 20, private burst = 20) {
    this.tokens = burst;
  }

  /** Resolves when a call is allowed. Callers are served in order. */
  acquire(): Promise<void> {
    const turn = this.queue.then(() => this.take());
    this.queue = turn;
    return turn;
  }

  private async take(): Promise<void> {
    for (;;) {
      const now = performance.now();
      this.tokens = Math.min(this.burst, this.tokens + ((now - this.updated) / 1000) * this.rate);
      this.updated = now;
      if (this.tokens >= 1) {
        this.tokens -= 1;
        return;
      }
      await new Promise((resolve) => setTimeout(resolve, ((1 - this.tokens) / this.rate) * 1000));
    }
  }
}
```

Call it before every request. Use one limiter for everything in the process that shares the key:

C#
```csharp
        // The limit is per API key: divide the rate between every process that shares the key.
        var limiter = new ConceptsRateLimiter(rate: 20, burst: 20);
        var page = await api.GetAsync("/groups?page=1&pageSize=50");
        foreach (var group in page.Data?.AsArray() ?? new JsonArray())
        {
            await limiter.AcquireAsync();
            var detail = (await api.GetAsync($"/groups/{group?["group_ID"]}")).Data;
            Console.WriteLine($"{detail?["group_ID"]} {detail?["name"]}");
        }
```

Python
```python
        # The limit is per API key: divide the rate between every process that shares the key.
        limiter = RateLimiter(rate=20, burst=20)
        page = api.get("/groups", {"page": 1, "pageSize": 50})
        for group in page.data:
            limiter.acquire()
            detail = api.get(f"/groups/{group['group_ID']}").data
            print(detail["group_ID"], detail.get("name"))
```

TypeScript
```typescript
  // The limit is per API key: divide the rate between every process that shares the key.
  const limiter = new RateLimiter(20, 20);
  const page = await api.get("/groups", { page: 1, pageSize: 50 });
  for (const group of page.data) {
    await limiter.acquire();
    const detail = (await api.get(`/groups/${group.group_ID}`)).data;
    console.log(detail.group_ID, detail.name);
  }
```

## Do more with fewer calls

| Instead of | Do this |
|  --- | --- |
| One create or update call per record | Send many records at once with a batch `PUT`, for example [`PUT /subscribers-batch`](/openapi/vbasoftware/vbasoftware.api/subscribers/updatebatchsubscribers) or [`PUT /subscribers/{subscriberID}/members-batch`](/openapi/vbasoftware/vbasoftware.api/members/updatebatchmembers). Check each item in the `207` response; see [Requests and responses](/developer-portal/concepts/requests-and-responses#batch-create-or-update-207-multi-status). Keep each batch small enough to finish well within your HTTP timeout. |
| Logging in before every call | Log in once and reuse the ID token until shortly before `expiresIn` runs out. See [Authentication and tokens](/developer-portal/concepts/authentication#reuse-the-token-until-it-expires). |
| Very large pages, or no paging | Page through lists near the default `pageSize` of 100, sorted on the key fields. See [Pagination and sorting](/developer-portal/concepts/pagination-and-sorting). |
| Adding claims to a claim queue one at a time | Add them in one call with [`POST /claim-queues/{claimQueueKey}/details-mass-insert`](/openapi/vbasoftware/vbasoftware.api/adv.-claim/massinsertclaimqueuedetail), then run the whole queue with [`POST /claim-queues/{claimQueueKey}/adjudicate`](/openapi/vbasoftware/vbasoftware.api/claim-adjudication/adjudicateclaimqueue) or [`POST /claim-queues/{claimQueueKey}/process`](/openapi/vbasoftware/vbasoftware.api/claim-adjudication/processclaimqueue). Both return `202` and work in the background. |
| Waiting for each single-claim adjudication result when you do not need it | [`POST /claim-batches/{batchNumber}/claims/{batchClaim}/adjudicate-no-response`](/openapi/vbasoftware/vbasoftware.api/claim-adjudication/adjudicateclaimnoresponse) returns `202` without the claim details, which the spec describes as a faster request. |
| Many `GET` calls to join data from several tables | One [`POST /search`](/openapi/vbasoftware/vbasoftware.api/search-request/searchrequest) returns chosen columns from related tables, up to 750 rows a page. See [Search API](/developer-portal/concepts/search). |
| Checking queued work in a tight loop | Wait between checks, and count them toward your limit. |


## Request a higher limit

If your integration needs more than the standard limit after batching, paging, and token reuse, ask VBA support for a service level review. Include:

- the expected production load: requests per second at normal and peak times, and when the peaks happen
- the integration design: which operations it calls, and how it uses batch operations, paging, and token reuse
- measurements from your integration, for example how often it receives `429` and at what request rate
- the business reason, for example a processing window that must finish overnight


Contacting support
Send VBA support:

- the `debug.activityID` from the response
- the date and time of the call, with time zone
- the method and path, for example `POST /claim-batches`
- the full `error` object from the response
- the request body, with passwords, tokens, and personal data removed


Never send your password, API key, or tokens. See [Support and escalation](/developer-portal/help/support).

## Related

- [Errors and troubleshooting](/developer-portal/concepts/errors): retrying `429` and other transient errors.
- [Pagination and sorting](/developer-portal/concepts/pagination-and-sorting): paging through large lists.
- [Requests and responses](/developer-portal/concepts/requests-and-responses): batch results and queued work.