If you’re seeing 429 errors while your average request rate is under your plan’s limit, the fix is usually in how your integration spaces its requests and not the size of your plan. This guide applies to all Trestle APIs, including Phone Validation API, Real Contact API, Address Validation API, Reverse Phone API, Reverse Address API, Caller Identification API, and Decision Signals API.
What You Will Learn #
- The difference between QPS and concurrency
- Why bursts trigger 429 errors even when your average rate is under your limit
- How to calculate the right thread count from your measured latency
- How to handle 429 responses with backoff
- Working code samples in Python, Node.js, and Java
What Is the Difference Between QPS and Concurrency? #
Your throughput and your concurrency are related, but they are not the same number. Treating them as interchangeable is a common cause of avoidable 429 errors.
What is QPS? #
QPS (queries per second) is the number of requests your integration sends to an API each second. Every Trestle account has a contracted QPS limit. For how limits are assigned, check out What is the Default and Max QPS Supported?
What is Concurrency? #
Concurrency is the number of requests your integration has in flight at the same time. In a threaded client, it’s your thread count. In an async runtime like Node.js, it’s the number of pending requests.
What is Round-Trip Latency? #
Round-trip latency is the time between sending a request and receiving the full response, as measured by your own client. It includes two parts:
- Trestle’s processing time. Published P50 and P95 values for each API are listed in What are the Response Times for Trestle’s APIs? and are measured inside Trestle’s firewall.
- Your network time to Trestle. Trestle’s APIs are hosted in the U.S.-West region. As a rule of thumb, add up to about 150ms of round-trip wait time for a coast-to-coast connection.
Only you can observe the network path, so always measure latency with a request timer in your own client.
How Are the Three Related? #
Your thread count, your target QPS, and your latency are tied together by how a thread works. Each thread sends a request, waits for the response, then sends the next. The longer each response takes, the fewer requests a single thread can send per second, and the more threads you need to reach your target.
The first formula calculates your thread count directly. Dividing your latency by 1000 converts it from milliseconds to seconds, and multiplying by your target QPS tells you how many requests need to be in flight at once:
Threads needed = Target QPS x (Round-trip latency in ms➗ 1000)
Equivalently, one thread sustains 1000➗ latency_ms QPS, so:
Threads needed = Target QPS ➗ (1000 ➗ latency_ms)
The second formula reaches the same answer in two steps. First, 1000 ➗ latency_ms tells you how many requests one thread can complete per second. Then, dividing your target QPS by that per-thread capacity tells you how many threads it takes to hit your target. Use whichever version is easier for you; both return the same number.
Only the latency changes between APIs. The formula and thread counts are the same for every Trestle API, whether a Phone Validation lookup or a Decision Signals API call. What differs is the processing time itself. Simpler calls typically complete faster than calls that enrich or cross-reference multiple data sources. Measure latency separately for each API you integrate, and size each integration’s thread pool independently.
Worked Example #
Target: 256 QPS. Measured round-trip latency for the API you’re calling: 400ms.
- One thread sustains 1000 ➗ 400 = 2.5 QPS
- Threads needed: 256 ➗ 2.5 = 102.4, rounded up to 103 threads
With 103 threads, each thread sends a request, waits about 400ms for the response, then sends the next. Across the threads, requests are naturally staggered, so the 256 QPS spreads evenly across the whole second instead of arriving in a spike.
By contrast, running 256 threads at 400ms latency makes you capable of 256 x 2.5 = 640 QPS, which is 2.5x your target, and that capacity arrives in bursts. Right-sizing to 103 threads keeps you on-target and smooth.
Your own number will differ by API, region, and network path. Use what you measure for the specific endpoint you’re integrating.
Quick Reference #
Threads needed for a target of 256 QPS at each measured latency (rounded up):
| Round-trip latency | QPS per thread | Threads needed |
| 100ms | 10.0 | 26 |
| 250ms | 4.0 | 64 |
| 300ms | 3.3 | 77 |
| 350ms | 2.9 | 90 |
| 400ms | 2.5 | 103 |
| 500ms | 2.0 | 128 |
| 600ms | 1.7 | 154 |
| 800ms | 1.25 | 205 |
| 1000ms | 1.0 | 256 |
For a different target QPS, scale proportionally:
Threads = (target QPS ➗ 256) x (value in the table)
What Happens if You Exceed Your Limit? #
Requests over your plan’s QPS limit will hit a 429 error. If your integration sends requests faster than your contracted QPS (on average or in a burst) on any Trestle API, the excess requests are rejected. A 429 error is a signal to pause briefly and retry with exponential backoff. Don’t retry immediately at the same rate, as that will continue to be rejected.
If you’re seeing a 429 error while your average QPS is comfortably under your plan’s limit, the cause is usually a burst pattern rather than an undersized plan. Revisit the thread sizing above before requesting a higher limit, or check out Getting 429: Too Many Requests Error – Why? to learn how QPS limits are assigned. If you need a higher tier, reach out to support@trestleiq.com.
Code Samples #
Each sample below starts a fixed pool of threads sized with the formula above, where every thread runs a closed loop: send a request, wait for the response, and then send the next. No manual delay is needed. Waiting for the response is the pacing.
Python #
import math, random, threading, time
import requests
# Placeholders: replace with your endpoint URL and your API key header.
API_URL = "https://api.trestleiq.com/..."
AUTH_HEADERS = {"x-api-key": "YOUR_API_KEY"}
TARGET_QPS = 256
LATENCY_S = 0.4 # your measured round-trip latency, in seconds
THREADS = math.ceil(TARGET_QPS * LATENCY_S) # ~103
running = True
def worker(session):
while running:
response = session.get(API_URL, headers=AUTH_HEADERS)
if response.status_code == 429:
time.sleep(1 + random.random()) # back off, then retry
continue
# process response; no sleep here -- the request itself paces the loop
threads = [
threading.Thread(target=worker, args=(requests.Session(),), daemon=True)
for _ in range(THREADS)
]
for t in threads:
t.start()
for t in threads:
t.join()
Node.js #
// Placeholders: replace with your endpoint URL and your API key header.
// Top-level await requires an ES module (.mjs, or "type": "module" in package.json).
const API_URL = "https://api.trestleiq.com/...";
const AUTH_HEADERS = { "x-api-key": "YOUR_API_KEY" };
const TARGET_QPS = 256;
const LATENCY_S = 0.4; // your measured round-trip latency, in seconds
const THREADS = Math.ceil(TARGET_QPS * LATENCY_S); // ~103
let running = true;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function worker() {
while (running) {
const res = await fetch(API_URL, { headers: AUTH_HEADERS });
if (res.status === 429) {
await sleep(1000 + Math.random() * 1000); // back off, then retry
continue;
}
// process response; no delay here -- awaiting fetch() paces the loop
}
}
await Promise.all(Array.from({ length: THREADS }, () => worker()));
Java #
// Placeholder: build `request` with your endpoint URL and your API key header.
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.trestleiq.com/..."))
.header("x-api-key", "YOUR_API_KEY")
.GET()
.build();
int targetQps = 256;
double latencySeconds = 0.4; // your measured round-trip latency, in seconds
int threads = (int) Math.ceil(targetQps * latencySeconds); // ~103
ExecutorService pool = Executors.newFixedThreadPool(threads);
AtomicBoolean running = new AtomicBoolean(true);
HttpClient client = HttpClient.newHttpClient();
for (int i = 0; i < threads; i++) {
pool.submit(() -> {
while (running.get()) {
try {
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 429) {
Thread.sleep(1000 + ThreadLocalRandom.current().nextInt(1000)); // back off, then retry
continue;
}
// process response; the blocking send() call paces the loop
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore interrupt status and stop this worker
return;
} catch (IOException e) {
// network error: log it, then continue the loop to retry
}
}
});
}Best Practices #
- Use a closed-loop client. Each thread should send one request, wait for the response, then send the next. Never fire all threads on a fixed timer at the top of each second.
- Measure your own latency. Use your observed average (P50) round-trip time. This includes your network path, not just Trestle’s published processing time.
- Recompute when latency changes. If your network latency changes, or you add hops such as a VPN, proxy, or gateway, your effective latency rises. International phone queries also run slower than U.S. queries. Plug the new number into the formula and adjust your thread count. If latency jumps unexpectedly, check API Live Status.
- Add light jitter. A few milliseconds of random start offset per thread prevents threads from accidentally drifting into alignment and forming bursts.
- If you use an open-loop rate limiter, pace it evenly. A token bucket limiter holds a fixed number of “tokens” that refill at a steady rate, and each request spends one. Set the refill rate to your target QPS so requests are spaced evenly. Never release a whole second’s budget at once.
Summary #
Threads do not equal QPS. Threads = QPS x latency (in seconds). That’s why you should size threads to your measured round-trip latency, so load spreads evenly across each second. Keep in mind that running more threads than the formula gives you will create bursts and 429s, not more usable throughput.
If you’ve sized your threads to your latency and are still seeing 429s, or you need a higher QPS tier, contact your account executive or email support@trestleiq.com.