Rate Limits

To keep the Utila API fast and reliable for everyone, requests are subject to rate limiting. This page describes how limits are applied, what happens when you exceed them, and how to build a client that handles them gracefully.

ℹ️

The specific limits below are the default values currently in effect and are provided for guidance only. They are not a contractual service level and may change over time as we tune the platform. Design your integration to react to the responses described here rather than assuming fixed thresholds.

Need higher throughput? Talk to us — limits can be raised for your workload as part of your commercial arrangement.

Who Is Rate Limited

Rate limits apply to requests authenticated with a service account (i.e. programmatic API access using an access token). Actions performed interactively in the Console are not affected by these API limits.

How Limits Are Applied

Utila uses a token bucket model. Each request consumes tokens from one or more buckets, and buckets refill continuously over time. This allows short bursts of traffic while keeping sustained throughput within the configured limits.

Every request is evaluated against several independent buckets, and a request is only allowed if all applicable buckets have capacity:

ScopeApplies toCurrently
Per service accountAll requests from a single service account1,000 requests / minute
Per vaultAll requests targeting a single vault1,000 requests / minute
Per endpointRequests to a single endpoint from a single service account100 requests / minute

Because the checks are independent, spreading traffic across multiple endpoints and vaults helps you stay within the per-endpoint limits while remaining under your overall account limit.

Request Cost

Most requests consume a single token. Some resource-intensive operations count for more than one request against your limits — for example, exporting wallet data. Requesting additional referenced resources on a response (where supported) may also increase the cost of a request, since related resources are fetched on your behalf.

Exceeding a Limit

When a limit is exceeded, the API responds with:

  • HTTP status 429 Too Many Requests (gRPC status RESOURCE_EXHAUSTED).
  • A retry-after header indicating the number of seconds to wait before retrying.
HTTP/1.1 429 Too Many Requests
retry-after: 5

A 429 is a signal to slow down, not a failure of your request logic — the request was not processed and can be safely retried after the indicated delay.

Handling Rate Limits

We recommend that clients:

  1. Respect retry-after. When present, wait at least the indicated number of seconds before retrying.
  2. Back off exponentially with jitter if a retry-after header is not available, to avoid retry storms.
  3. Limit client-side concurrency and smooth out bursts rather than sending large batches of requests simultaneously.
  4. Cache responses that do not change frequently instead of re-requesting them.

Example

import random
import time
import requests

def request_with_retry(session, method, url, max_retries=5, **kwargs):
    for attempt in range(max_retries):
        res = session.request(method, url, **kwargs)
        if res.status_code != 429:
            res.raise_for_status()
            return res

        # No point sleeping after the final attempt — we're about to give up.
        if attempt == max_retries - 1:
            break

        # Prefer the server-provided delay, otherwise back off exponentially
        # with full jitter to avoid synchronized retries.
        retry_after = res.headers.get("retry-after")
        delay = float(retry_after) if retry_after else random.uniform(0, min(2 ** attempt, 30))
        time.sleep(delay)

    raise RuntimeError("rate limit exceeded after retries")
async function requestWithRetry(url, options = {}, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const res = await fetch(url, options);
    if (res.status !== 429) {
      return res;
    }

    // No point sleeping after the final attempt — we're about to give up.
    if (attempt === maxRetries - 1) {
      break;
    }

    // Prefer the server-provided delay, otherwise back off exponentially
    // with full jitter to avoid synchronized retries.
    const retryAfter = res.headers.get("retry-after");
    const delaySeconds = retryAfter ? Number(retryAfter) : Math.random() * Math.min(2 ** attempt, 30);
    await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000));
  }

  throw new Error("rate limit exceeded after retries");
}

Network-Level Protection

Independently of the per-account limits above, Utila's edge enforces network-level protections against denial-of-service and abusive traffic. These are applied per source IP address and also respond with 429 Too Many Requests when tripped. Unlike the per-account API limits, responses from this layer do not include a retry-after header — clients should fall back to exponential backoff with jitter in this case.

This layer is intended only to absorb abnormal traffic spikes; legitimate integrations operating within the API limits above will not encounter it under normal conditions. If you consistently run large workloads from a single IP behind a shared gateway or proxy, contact us so we can help you plan accordingly.