Domain API Rate Limit, Soft Quota & Throttling Policy

Two numbers run everything on this page: 2 and 200. Two requests per second, two hundred quota points per minute. 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 reseller account. 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 2 requests per second; go faster and you get HTTP 429. Separately, every call costs points:
/domains/search and /domains/info cost 1 point, every other endpoint costs 10. You get 200 points per 60 seconds. Fill 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. Both limits are counted per reseller, not per API key.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 — 2 requests / second
This one counts requests, not value. Two per second is the ceiling for your whole account, summed across every thread, worker, container and cron job you run.
- Breaking it returns HTTP 429 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 / 60 seconds
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
__reseller identifier, so all of them land in the same bucket no matter which key signed them.What Each Request Costs
Two endpoints 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. 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 200 names costs 200 points one at a time, or 40 points in four 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. The quota would let you spend that in two and a half minutes — but at 2 requests per second the wall clock is 250 seconds anyway, so the rate limit is what governs and the brake never engages. Through bulk-search in batches of 50, the same job is 10 calls, 100 points and about 5 seconds. Same answer, fifty times faster, and it spends 100 of your points instead of 500.
Worked example: registering 100 domains
A hundred registrations cost 1,000 points, and the quota releases 200 points a minute. That job takes at least five minutes, and no amount of parallelism changes it. Fire them off at full speed and you will fill the quota in the first ten seconds, then crawl through the remaining ninety behind a growing delay. Pace them at one every three seconds and you finish in the same five minutes with every response arriving promptly. The paced version is not slower — it is just honest about how long the work takes.
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.
How to Fix an HTTP 429
A 429 only ever means one thing here: more than two requests left your side within the same second. It has nothing to do with the quota and nothing to do with how many domains you manage. 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.
- Honour
Retry-Afterif the response carries it, and wait at least one second if it does not. - Retry once. A single 429 after a burst is normal and one retry usually clears it.
- If it repeats, back off exponentially — 1 s, 2 s, 4 s, 8 s — with a sensible ceiling.
- 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 “2 per second” are really sending eight.
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; if your workload is made of 10-point calls, raise the interval to 3 seconds and you will never meet the brake.
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 for 1-point calls. Use 3000 ms for 10-point calls.
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. Use 3000000 for 10-point calls.
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 for 1-point calls. Use 3.0 for 10-point calls.
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; use 3000 for 10-point calls
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 2 requests this second? The answer is HTTP 429 and the request stops here.
3. Quota check
Points are added to your 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, 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. It is the earliest signal available to you, and it climbs 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 “2 per second” on their own, so eight workers send sixteen.
- Looping
/domains/searchover a 500-name list when fourbulk-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.
- Creating extra API keys expecting extra allowance — the counter follows the reseller, not the key.
- Assuming
async, threads or separate containers get separate budgets. They do not; they all carry the same__resellerheader.
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.
