Rate Limits

View as markdown

Synthient enforces two distinct limits: lookup credits for the synchronous IP and domain APIs, and per-endpoint request-rate limits that apply to every HTTP call. Both are per-team.

Lookup credits

Every call to the IP API or domain lookup consumes credits from your plan's monthly allotment. Inspect the remaining balance and reset cadence on /account/me:

{
  "lookup_quota": {
    "credits": 982341,
    "resets_in": 1893456
  }
}

credits is the integer balance remaining; resets_in is the seconds until the balance refills.

How requests are charged

EndpointCost
GET /api/v4/lookup/ip/{ip}1 credit per request.
POST /api/v4/lookup/ipsceil(n × 0.9) credits, where n is the number of unique, valid IPs in the body (10% batch discount).
GET /api/v4/lookup/domain/{domain}1 credit per request.
Feed exports (/feeds/{stream}/export*)Free, counted against your feed subscription, not lookup credits.
Feed streams (/feeds/{stream}/stream)Free, counted against your concurrent-stream allowance.

Worked example

A batch request with 100 IPs costs ceil(100 × 0.9) = 90 credits versus 100 credits as individual lookups, a 10-credit saving. A batch with 1,000 IPs costs 900 credits.

Duplicate and invalid entries are stripped before charging, so they don't count toward your bill. Track your remaining balance via /account/me.

Out of credits

When credits reaches zero, lookup endpoints return 402 Payment Required. Either add credits, upgrade your plan, or wait for resets_in to elapse. Streaming and export endpoints are unaffected; they don't consume lookup credits.

Per-endpoint rate limits

Every endpoint is rate-limited per team (the API key's owning organization), not per IP, so multiple keys for the same team share the same buckets. The defaults below are sized for a healthy production caller; reach out to support before designing around higher throughput.

EndpointSustained rateBurst
GET /lookup/ip/{ip} and POST /lookup/ips100 req/sec200
GET /lookup/domain/{domain}20 req/min20
GET /account/me10 req/sec10
GET /feeds/{stream}/stream (connection establishment)0.5 req/sec20
GET /feeds/{stream}/export and …/meta2 req/sec60
GET /feeds/{stream}/export/{snapshot_id} (downloads)0.1 req/sec (≈360/hour)120

Stream and export buckets are shared across all eight feeds for a team; fanning out to every stream doesn't multiply the budget.

Excess requests get 429 Too Many Requests. Long-lived stream connections are not re-charged once established; only the initial GET counts against the connection-rate bucket.

Rate-limit response headers

Every rate-limited HTTP response carries the standard IETF RateLimit headers:

HeaderMeaning
RateLimit-LimitTotal tokens in the bucket.
RateLimit-RemainingTokens left after this request.
RateLimit-ResetSeconds until the bucket fully refills.

Read these headers to pace your calls instead of polling for 429s.

Retry guidance

Retry policy depends on the endpoint. The SDKs use these rules:

  • IP, batch, and domain lookups, plus health: retry only 429 responses that carry Retry-After. A server error or lost connection can leave a lookup's outcome unknown, so automatically repeating it risks another charge.
  • Account and feed requests: also retry 500, 502, 503, 504, and transient connection failures. Errors while reading an established stream or download are surfaced to the caller.
  • 429 without Retry-After: stop. A missing header indicates an exhausted monthly request budget or too many concurrent streams; retrying in a loop will not resolve either condition.

The SDKs allow two retries by default, for three total attempts. Start the backoff at 1 second, double it up to a 30-second base, then apply ±25% jitter. The actual wait is the larger of the jittered backoff and Retry-After, so jitter never shortens the server's requested delay. Retry-After accepts seconds or an HTTP date. If it asks for more than 60 seconds, surface the error instead of retrying early.

This HTTP lookup example follows the same policy:

Retry with backoff

import os
import random
import re
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime

import requests

URL = "https://api.synthient.com/api/v4/lookup/ip/8.8.8.8"
HEADERS = {"x-api-key": os.environ["SYNTHIENT_API_KEY"]}

def parse_retry_after(value):
    if value is None:
        return None
    value = value.strip()
    if re.fullmatch(r"-?[0-9]+", value):
        return max(0.0, float(value))
    try:
        when = parsedate_to_datetime(value)
    except (TypeError, ValueError, OverflowError):
        return None
    if when.tzinfo is None:
        when = when.replace(tzinfo=timezone.utc)
    return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())


def lookup_with_retry(max_attempts: int = 3):
    if max_attempts < 1:
        raise ValueError("max_attempts must be positive")
    for attempt in range(max_attempts):
        with requests.get(URL, headers=HEADERS, timeout=10) as response:
            if response.status_code < 400:
                return response.json()
            retry_after = parse_retry_after(response.headers.get("Retry-After"))
            if (
                response.status_code != 429
                or retry_after is None
                or retry_after > 60
                or attempt == max_attempts - 1
            ):
                response.raise_for_status()
            base = min(30, 2 ** min(attempt, 5))
            delay = max(base * random.uniform(0.75, 1.25), retry_after)
        time.sleep(delay)

Streaming reconnects

Long-lived streams are closed cleanly by the server every ~30 minutes. Reconnect immediately after a clean close. If establishing the next connection fails, apply the account/feed retry policy above. Stop on permanent errors and on 429 without Retry-After. A failure while reading an established stream is surfaced by the SDK; there is no resume cursor, so reconnecting can miss events.

See the Firehose page for full NDJSON consumer examples in Python and Go.

What to instrument

A small amount of client-side telemetry pays for itself the first time you hit a limit:

  • Per-status counters: track 2xx, 4xx, 5xx rates separately. A creeping 429 rate is the earliest sign you should batch or cache.
  • Backoff distribution: record actual wait durations. Frequent long waits indicate that traffic needs pacing; preserve Retry-After as the minimum wait.
  • Quota gauge: pull lookup_quota.credits periodically and alert on burn rate, not just zero.

Next steps

  • Errors: full status code reference and response shapes.
  • Authentication: scope and key management.
  • Account: programmatic quota inspection.