When you integrate an email verification API into your application, the integration itself is usually straightforward — make a POST request, get a JSON response. But what happens when you need to verify 100,000 emails? Or when the API returns a rate limit error? Or when you want real-time verification without blocking your signup flow?
This guide covers the three things every production email verification integration needs: smart rate limiting, webhook-driven async processing, and robust error handling. Includes working code examples in Python, JavaScript, and PHP.
Architecture Patterns for Email Verification APIs
Pattern 1: Synchronous (Real-Time) Verification
Use when: Verifying a single email in real-time — signup forms, checkout flows, lead capture.
Client → Your Server → Verification API → Response → Your Server → Client
Latency: 200–800ms (one SMTP round-trip). Fast enough for most UX but noticeable if called inline.
Pattern 2: Asynchronous (Batch) Verification
Use when: Verifying lists of 1,000+ emails — bulk imports, list cleaning, scheduled jobs.
Client → Your Server → Queue Job → Worker → Verification API → Store Results
↓
Client polls or receives webhook when complete
Pattern 3: Webhook-Driven Verification
Use when: You want real-time verification without holding the client connection open.
Client → Your Server → Submit Job (returns job_id)
↓
Your Server → Verification API → POST webhook → Update database
↓
Client polls or receives notification when job completes
Rate Limiting: How to Verify Millions Without Getting Blocked
Understanding Rate Limits
| Limit Type | Example | Enforcement |
|---|---|---|
| Requests per second (RPS) | 50 RPS | Token bucket — burst to limit, then wait |
| Requests per minute (RPM) | 500 RPM | Sliding window |
| Requests per day | 50,000/day | Fixed window, resets at midnight UTC |
| Concurrent connections | 10 simultaneous | Connection pool limit |
Strategy 1: Token Bucket Rate Limiter (Python)
import time
import threading
class TokenBucket:
"""Rate limiter that allows bursts up to `capacity`, refills at `rate` per second."""
def __init__(self, rate: float, capacity: int):
self.rate = rate
self.capacity = capacity
self.tokens = capacity
self.last_refill = time.monotonic()
self.lock = threading.Lock()
def acquire(self) -> bool:
with self.lock:
now = time.monotonic()
elapsed = now - self.last_refill
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.last_refill = now
if self.tokens >= 1:
self.tokens -= 1
return True
return False
def wait_and_acquire(self) -> None:
while not self.acquire():
time.sleep(0.05)
# Usage: 50 requests per second, burst up to 100
rate_limiter = TokenBucket(rate=50, capacity=100)
def verify_email(email: str) -> dict:
rate_limiter.wait_and_acquire()
response = requests.post(
"https://api.email-validator.com/v1/verify",
json={"email": email},
headers={"Authorization": f"Bearer {API_KEY}"}
)
return response.json()
Strategy 2: Queue + Worker Pool (Bulk Processing)
from concurrent.futures import ThreadPoolExecutor, as_completed
from queue import Queue
def bulk_verify(emails: list[str], max_workers: int = 5, max_rps: int = 50) -> dict:
limiter = TokenBucket(rate=max_rps, capacity=max_rps * 2)
results = {}
queue = Queue()
for email in emails:
queue.put(email)
def worker():
while not queue.empty():
try:
email = queue.get_nowait()
except:
break
limiter.wait_and_acquire()
try:
result = verify_email(email)
results[email] = result
except Exception as e:
results[email] = {"error": str(e)}
finally:
queue.task_done()
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [executor.submit(worker) for _ in range(max_workers)]
for future in as_completed(futures):
future.result()
return results
Strategy 3: Exponential Backoff (JavaScript/Node.js)
async function verifyEmailWithRetry(email, maxRetries = 5) {
let attempt = 0;
while (attempt < maxRetries) {
const response = await fetch('https://api.email-validator.com/v1/verify', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
if (response.status === 429) {
attempt++;
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.min(1000 * Math.pow(2, attempt), 60000);
console.warn(`Rate limited. Retrying in ${delay}ms (${attempt}/${maxRetries})`);
await sleep(delay);
continue;
}
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return await response.json();
}
throw new Error(`Failed after ${maxRetries} retries`);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Webhooks: Async Verification Without Polling
Implementing a Webhook Receiver (PHP)
<?php
// webhook_receiver.php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_EMAILVERIFY_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $payload, getenv('WEBHOOK_SECRET'));
if (!hash_equals($expected, $signature)) {
http_response_code(401);
die('Invalid signature');
}
$data = json_decode($payload, true);
if (!isset($data['job_id'], $data['status'], $data['results'])) {
http_response_code(400);
die('Invalid payload');
}
$db = new PDO('mysql:host=localhost;dbname=email_verifier', 'user', 'pass');
foreach ($data['results'] as $result) {
$stmt = $db->prepare('
UPDATE email_queue
SET status = :status, result = :result, verified_at = NOW()
WHERE email = :email AND job_id = :job_id
');
$stmt->execute([
'status' => $result['status'],
'result' => json_encode($result),
'email' => $result['email'],
'job_id' => $data['job_id'],
]);
}
http_response_code(200);
echo json_encode(['received' => true]);
Webhook Security
| Practice | Why |
|---|---|
| HMAC signature verification | Proves request came from the API |
| IP whitelisting | Only accept from known IP ranges |
| Idempotency keys | Prevent processing duplicate webhooks |
| Retry handling | Webhooks can arrive multiple times |
| Timely acknowledgment | Respond 200 quickly, process async |
Error Handling
HTTP Status Codes and Your Response
| Status | Error | What to Do |
|---|---|---|
| 400 | Bad Request | Validate input client-side |
| 401 | Unauthorized | Check API key, don't retry |
| 402 | Payment Required | Upgrade plan or wait for quota reset |
| 429 | Too Many Requests | Exponential backoff |
| 422 | Unprocessable | Valid result — unverifiable email, not a failure |
| 500 | Internal Server Error | Retry with backoff, alert monitoring |
| 503 | Service Unavailable | Retry after Retry-After header value |
Robust Error Handler (Python)
import time
import requests
class VerificationAPIError(Exception):
pass
def verify_email_safe(email: str, max_retries: int = 3) -> dict:
retryable = {429, 500, 502, 503}
for attempt in range(1, max_retries + 1):
try:
response = requests.post(
"https://api.email-validator.com/v1/verify",
json={"email": email},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=10
)
if response.status_code == 200:
return response.json()
if response.status_code == 401:
raise VerificationAPIError("Invalid API key")
if response.status_code == 402:
raise VerificationAPIError("Quota exceeded")
if response.status_code == 422:
return response.json() # valid result, not an error
if response.status_code in retryable:
if attempt == max_retries:
raise VerificationAPIError(f"Unavailable after {max_retries} attempts")
time.sleep(min(2 ** attempt, 30))
continue
raise VerificationAPIError(f"Unexpected status: {response.status_code}")
except requests.exceptions.Timeout:
if attempt == max_retries:
raise VerificationAPIError("Request timed out repeatedly")
time.sleep(min(2 ** attempt, 30))
return None
Production Checklist
□ Rate limiting implemented (token bucket or queue-based)
□ Exponential backoff on 429/5xx responses
□ Webhook signature verification (HMAC-SHA256)
□ Webhook idempotency (job_id + event_id deduplication)
□ All API calls have timeouts (5–10 seconds max)
□ API key stored in environment variables only
□ Error logging with retry counts
□ Monitoring/alerting for sustained error rate increase
□ Graceful degradation if API is down (don't block user signups)
Key Takeaways
- Synchronous for signup forms; async + webhooks for bulk lists
- Token bucket handles bursts gracefully — implement before you need it
- Exponential backoff on 429/5xx — 1s → 2s → 4s → 8s → max 60s
- Verify webhook signatures with HMAC — never trust incoming POSTs
- 422 is a valid result — unverifiable emails aren't API failures
- Never block user signups if the API is down — queue and retry
- Set timeouts on every call — default TCP timeouts can be 60+ seconds