Domain API Rate Limit, Soft Quota & Throttling Policy

Two numbers run everything on this page: 5 and 200. Five requests per second per API key, and a quota threshold of two hundred points per area. Learn those and you already know the policy — the rest is arithmetic and a handful of code snippets.
Domain Name API applies two independent controls to every API key. The first is a hard rate limit: exceed it and you get an HTTP 429. The second is a soft quota, and it behaves nothing like the first — it never rejects a request, it slows one down. Understanding that difference is the whole point of this article, because an integration that confuses the two will spend an afternoon chasing a bug that does not exist.
Send at most 5 requests per second across all endpoints, and no more than 2 per second of them to
/domains/search and /domains/bulk-search; go faster and you get HTTP 429 with a Retry-After header. Separately, every call costs points: lookups (/domains/search, /domains/info, /products/info) cost 1 point, every other endpoint costs 10. Your points decay by 20% per minute, and below 200 points nothing happens. Go above that and nothing fails — your responses simply start arriving about half a second late, climbing to a 5-second ceiling if you keep the pace. Ease off and the delay winds back down. The limits are counted per API key, and points are tracked separately for each area.Contents
- Rules at a glance
- Two layers: rate limit vs. soft quota
- What each request costs
- Your real budget, in numbers
- What happens when the quota fills
- How to fix an HTTP 429
- Code examples: C#, PHP, Python, Node.js
- Request flow diagram
- Best practices that actually help
- Common integration mistakes
- Frequently asked questions
- Why these limits exist
- Requesting a higher limit
Rules at a Glance
Everything the policy says, in one table. Forwarding this table to your integration team is usually enough.
Two Layers: Rate Limit vs. Soft Quota
These two controls are measured differently, react differently and are fixed differently. Almost every support ticket on this topic begins with someone treating them as one thing.
Rate limit — 5 requests / second
This one counts requests, not value. Five per second is the ceiling for each API key, summed across every thread, worker, container and cron job that uses it — and within those five, domain searches (search and bulk-search) may use at most two.
- Breaking it returns HTTP 429 with a
Retry-Afterheader, and the request does not run - It reacts instantly — a single busy second is enough
- The fix is one shared limiter in your code
Soft quota — 200 points, decaying 20% a minute
This one counts value, not requests. A cheap lookup and an expensive registration are not the same load on the systems behind us, so they do not carry the same price.
- Breaking it delays your responses — nothing is rejected
- It is a brake, not a penalty, and it releases on its own
- The fix is pacing, or fewer expensive calls
What Each Request Costs
Lookups are cheap because they are read-only and they are the ones your customers trigger live on your website while they are deciding what to buy. Everything else reaches a registry, a billing record or a certificate authority, so it carries the real cost.
bulk-search surprises people. It costs a flat 10 points no matter how many names are in the array (up to 30 per call). Ten names is the break-even point: below ten, looping /domains/search is cheaper; at exactly ten it is a tie; above ten, bulk-search wins and keeps winning. Checking 300 names costs 300 points one at a time, or 100 points in ten bulk calls.Your Real Budget, in Numbers
The two limits interact, and which one you actually meet depends entirely on what you are calling. This is the table worth pinning above your desk.
Worked example: checking 500 domains
One at a time through /domains/search, that is 500 calls and 500 points. At the 2-per-second search cap the wall clock is at least 250 seconds, and somewhere past the first 200 lookups the points quota starts adding delay as well. Through bulk-search in batches of 30, the same job is 17 calls and 170 points — under the 200-point threshold, so it runs at full speed in about 9 seconds. Same answer, far faster, and it spends 170 of your points instead of 500.
Worked example: registering 100 domains
A hundred registrations cost 1,000 points in the domains area. From a standing start, about the first 20 run at full speed; after that each response is held back — starting at half a second and rising to a ceiling of 5 seconds — for as long as you stay above the threshold. Nothing is rejected, so the job still completes; it simply takes minutes rather than seconds, and no amount of parallelism changes that, because every worker using the same key draws on the same points. Plan for it: run large batches off-peak, keep your client timeout well above 5 seconds, and watch X-Quota-Delay rather than retrying harder.
bulk-search (up to 30 names for 10 points) is the cheaper route.What Happens When the Quota Fills
Nothing dramatic, which is exactly the problem — the brake is quiet, and quiet symptoms are the ones that cost you an afternoon. Every response tells you where you stand: X-Quota-Scope (the area), X-Quota-Cost (1 or 10), X-Quota-Used, X-Quota-Limit (200), X-Quota-Delay (seconds of delay applied; 0 means full speed) and X-Quota-Reset (seconds until you are back under the threshold).
How to Fix an HTTP 429
A 429 only ever means one thing here: one of your API key’s per-second caps was exceeded — more than five requests in the same second, or more than two domain searches. It has nothing to do with the points quota and nothing to do with how many domains you manage. The response has an empty body and a Retry-After header, and a rejected request costs no points. Work through these in order.
- Stop sending. Continuing to hammer the endpoint keeps you above the line and turns one 429 into a stream of them.
- Wait the number of seconds in
Retry-After(always a whole number, at least 1), then retry. - Retry once. A single 429 after a burst is normal and one retry usually clears it.
- If it repeats, pace instead of retrying — send requests one after another through a single queue rather than in bursts.
- Then find the real cause. Nine times out of ten it is parallel workers that each carry their own rate limiter, so four workers at “5 per second” are really sending twenty.
Code Examples
The shape that works is always the same: one gate for the whole process, a minimum interval between requests, and a retry that waits instead of insisting. These examples pace at two requests per second, which keeps you inside both the 5-per-second cap and the 2-per-second search cap. For long batches of 10-point calls, a gap of about 15 seconds (4 a minute) keeps you under the points threshold indefinitely.
C# (.NET)
// One gate for the whole process. Every worker passes through it.
private static readonly SemaphoreSlim Gate = new(1, 1);
private static DateTime _next = DateTime.UtcNow;
// 500 ms -> 2 req/sec: inside the 5/sec cap and the 2/sec search cap.
private static readonly TimeSpan MinInterval = TimeSpan.FromMilliseconds(500);
async Task<HttpResponseMessage> SendAsync(Func<HttpRequestMessage> build)
{
var backoff = TimeSpan.FromSeconds(1);
for (var attempt = 1; attempt <= 6; attempt++)
{
await Gate.WaitAsync();
try
{
var wait = _next - DateTime.UtcNow;
if (wait > TimeSpan.Zero) await Task.Delay(wait);
_next = DateTime.UtcNow + MinInterval;
}
finally { Gate.Release(); }
// HttpRequestMessage is single-use: build a fresh one per attempt.
var response = await _http.SendAsync(build());
if ((int)response.StatusCode != 429) return response;
var after = response.Headers.RetryAfter?.Delta ?? backoff;
response.Dispose();
await Task.Delay(after);
backoff = TimeSpan.FromSeconds(Math.Min(backoff.TotalSeconds * 2, 30));
}
throw new HttpRequestException("Rate limit did not clear after 6 attempts.");
} PHP (WHMCS, WordPress, cPanel)
<?php
// 500000 microseconds = 0.5 s -> 2 req/sec (inside the 5/sec and 2/sec search caps).
const DNA_MIN_INTERVAL_US = 500000;
function dna_request(string $method, string $path, ?array $body = null) {
static $next = 0;
$backoff = 1;
for ($attempt = 1; $attempt <= 6; $attempt++) {
$now = (int) (microtime(true) * 1000000);
if ($now < $next) usleep($next - $now);
$next = (int) (microtime(true) * 1000000) + DNA_MIN_INTERVAL_US;
$ch = curl_init('https://api.domainresellerapi.com/api/v1' . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30, // never below 10
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'__reseller: ' . DNA_RESELLER_ID,
'X-API-KEY: ' . DNA_API_KEY,
],
CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body),
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status !== 429) return json_decode($raw, true);
sleep($backoff);
$backoff = min($backoff * 2, 30);
}
throw new RuntimeException('Rate limit did not clear after 6 attempts.');
} Python (scripts and automation)
import threading, time, requests
BASE = "https://api.domainresellerapi.com/api/v1"
HEADERS = {"__reseller": RESELLER_ID, "X-API-KEY": API_KEY}
# 0.5 s -> 2 req/sec: inside the 5/sec cap and the 2/sec search cap.
MIN_INTERVAL = 0.5
_lock, _next = threading.Lock(), 0.0
def dna_request(method, path, **kwargs):
global _next
backoff = 1.0
for _ in range(6):
with _lock: # one gate, every thread
wait = _next - time.monotonic()
if wait > 0:
time.sleep(wait)
_next = time.monotonic() + MIN_INTERVAL
# timeout must clear the 5 s soft-quota ceiling with room to spare
r = requests.request(method, BASE + path, headers=HEADERS,
timeout=30, **kwargs)
if r.status_code != 429:
return r.json()
time.sleep(float(r.headers.get("Retry-After", backoff)))
backoff = min(backoff * 2, 30)
raise RuntimeError("Rate limit did not clear after 6 attempts.") Node.js (JavaScript / TypeScript)
const BASE = 'https://api.domainresellerapi.com/api/v1';
const MIN_INTERVAL = 500; // 2 req/sec: inside the 5/sec and 2/sec search caps
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// A promise chain is the simplest single gate in Node: awaiting `tail`
// serialises every caller, no matter how many started in parallel.
let tail = Promise.resolve();
function pace() {
const turn = tail.then(() => sleep(MIN_INTERVAL));
tail = turn;
return turn;
}
export async function dnaRequest(method, path, body) {
let backoff = 1000;
for (let attempt = 1; attempt <= 6; attempt++) {
await pace();
const res = await fetch(BASE + path, {
method,
headers: {
'Content-Type': 'application/json',
__reseller: process.env.DNA_RESELLER_ID,
'X-API-KEY': process.env.DNA_API_KEY,
},
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(30000), // never below 10000
});
if (res.status !== 429) return res.json();
const after = Number(res.headers.get('Retry-After')) * 1000 || backoff;
await sleep(after);
backoff = Math.min(backoff * 2, 30000);
}
throw new Error('Rate limit did not clear after 6 attempts.');
} Request Flow Diagram
Every call travels the same path. The two checkpoints are independent, and only one of them can reject you.
1. Your queue
One shared gate releases a request. Nothing else in your process is allowed to bypass it.
2. Rate check
More than 5 requests this second on your key, or more than 2 searches? The answer is HTTP 429 with Retry-After, and the request stops here.
3. Quota check
Points are added to the area’s running total, which decays 20% a minute. Over 200? The response is held back, then sent anyway.
4. Your handler
A 429 means back off and retry. A slow 200 means slow down — nothing is broken.
Best Practices That Actually Help
- One limiter per process, not per worker. If each thread carries its own, your real rate is the limit multiplied by your thread count. This single mistake causes most 429s.
- Reach for
bulk-searchabove ten names. One call, up to 30 names, 10 points, one round trip. The saving compounds fast on long lists. - Cache aggressively, and de-duplicate before you send. A name you checked four seconds ago has not changed, and long input lists are full of repeats.
- Keep the client timeout at 10 seconds or more. It has to clear the 5-second ceiling with room to spare, or the brake reaches you disguised as a network error.
- Stagger your scheduled jobs. If every cron in your fleet starts at the top of the minute, you have built a burst on purpose. A random offset of a few seconds costs nothing and fixes it.
- Watch your own 429 rate and the
X-Quota-Delayheader. They are the earliest signals available to you, and they climb long before a customer notices anything. - Split your queues by cost, not by count. Give registrations and renewals a slow lane of their own and let live availability checks run at full speed. Mixing them behind one pace penalises your website for your batch jobs.
Common Integration Mistakes
- Running parallel workers that each enforce “5 per second” on their own, so four workers on one key send twenty.
- Looping
/domains/searchover a 500-name list when seventeenbulk-searchcalls would answer the same question. - Reading the soft-quota delay as a network fault and lowering the timeout, which converts a slow success into a hard failure.
- Retrying immediately after a 429, which guarantees another one.
- Re-checking the same domain over and over inside a short window.
- Sharing one API key across many unrelated jobs and wondering why they slow each other down — everything on one key shares its per-second cap, and everything in one area shares its points.
- Assuming
async, threads or separate containers get separate budgets. They do not; they all send the same API key.
Frequently Asked Questions
Why These Limits Exist
Not to slow you down — you are welcome to automate everything, and most of our largest resellers do. The constraint sits further upstream. Behind our API are registries, and several of them are considerably less forgiving than we are. When a burst reaches a registry faster than it wants to be reached, it does not throttle politely; it starts returning errors. Those errors land on your customer’s registration attempt, and the failure looks like yours, not the registry’s.
The soft quota exists so we can absorb that pressure instead of passing it on. Half a second of added latency is a much better outcome than a failed registration, and it is a far better outcome than the alternative most APIs reach for, which is to reject the request outright. We would rather your batch job take five minutes than have it take two and lose eleven domains along the way.
It also keeps the API open. A shared platform with no brake ends up with approval gates and manual reviews before anyone is allowed to automate anything. The brake is what lets us keep saying yes.

