Rate Limits
Understand and manage API rate limits to ensure reliable service and optimal usage.
Overview
Matchstra enforces rate limits based on your license plan to ensure fair usage and system stability.
Key concepts:
- Request quota: Total requests allowed in your billing period
- Rate limiting: No per-second/per-minute throttling (quota-based only)
- Remaining requests: Returned in successful API responses
How Rate Limits Work
Quota-Based System
Your license plan includes a fixed number of requests per billing period (typically monthly).
Example plans:
| Plan | Monthly Quota | Overages |
|---|---|---|
| Starter | 500 requests | Not available |
| Professional | 5,000 requests | $0.10/request |
| Business | 25,000 requests | $0.08/request |
| Enterprise | Custom | Custom pricing |
Actual plans and pricing may vary. Check your dashboard for your specific quota.
Checking Remaining Quota
Every successful API response includes remainingRequests:
{
"success": true,
"message": "Screening completed successfully",
"data": {
"id": 12345,
"hasMatch": false,
// ... other fields
"remainingRequests": 485
}
}
Monitoring:
async function makeAPICall(url, options) {
const response = await fetch(url, options);
const data = await response.json();
if (data.success && data.data.remainingRequests !== undefined) {
const remaining = data.data.remainingRequests;
console.log(`Remaining requests: ${remaining}`);
if (remaining < 100) {
console.warn('⚠️ Low quota warning: Less than 100 requests remaining');
}
if (remaining < 10) {
console.error('🚨 Critical: Less than 10 requests remaining!');
alertAdmin('API quota nearly exhausted');
}
}
return data;
}
Rate Limit Exceeded (429)
When your quota is exhausted, the API returns HTTP 429:
{
"success": false,
"message": "Rate limit exceeded",
"data": null,
"errors": ["Request quota exhausted for current billing period"]
}
Response Headers:
HTTP/1.1 429 Too Many Requests
Retry-After: 86400
Content-Type: application/json
The Retry-After header indicates seconds until quota resets (typically at billing period renewal).
Handling 429 Errors
async function callWithRateLimitHandling(url, options) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '3600', 10);
const retryDate = new Date(Date.now() + retryAfter * 1000);
console.error(`Rate limit exceeded. Quota resets at ${retryDate.toISOString()}`);
throw new RateLimitError(
`API quota exhausted. Resets in ${Math.ceil(retryAfter / 3600)} hours`,
retryAfter
);
}
return response;
}
class RateLimitError extends Error {
constructor(message, retryAfter) {
super(message);
this.name = 'RateLimitError';
this.retryAfter = retryAfter;
}
}
// Usage
try {
const result = await callWithRateLimitHandling(url, options);
} catch (error) {
if (error instanceof RateLimitError) {
// Show user-friendly message
showNotification(
`You've reached your API limit. Quota resets in ${Math.ceil(error.retryAfter / 3600)} hours.`,
'warning'
);
// Or upgrade prompt
showUpgradePrompt();
}
}
Best Practices
1. Monitor Quota Proactively
Track remaining quota and set up alerts:
class QuotaMonitor {
constructor(warningThreshold = 0.1, criticalThreshold = 0.05) {
this.warningThreshold = warningThreshold; // 10%
this.criticalThreshold = criticalThreshold; // 5%
this.initialQuota = null;
}
checkQuota(remainingRequests) {
if (this.initialQuota === null) {
this.initialQuota = remainingRequests;
}
const percentRemaining = remainingRequests / this.initialQuota;
if (percentRemaining < this.criticalThreshold) {
this.sendAlert('critical', remainingRequests);
} else if (percentRemaining < this.warningThreshold) {
this.sendAlert('warning', remainingRequests);
}
}
sendAlert(level, remaining) {
console[level === 'critical' ? 'error' : 'warn'](
`${level.toUpperCase()}: Only ${remaining} API requests remaining!`
);
// Send email, Slack notification, etc.
notifyAdmins({
level,
remaining,
message: `Matchstra API quota at ${level} level`
});
}
}
// Usage
const monitor = new QuotaMonitor();
async function makeAPICall(url, options) {
const response = await fetch(url, options);
const data = await response.json();
if (data.success && data.data.remainingRequests) {
monitor.checkQuota(data.data.remainingRequests);
}
return data;
}
2. Implement Request Queuing
Queue requests when approaching quota limits:
class RequestQueue {
constructor(maxQueueSize = 100) {
this.queue = [];
this.maxQueueSize = maxQueueSize;
this.processing = false;
}
async enqueue(requestFn) {
if (this.queue.length >= this.maxQueueSize) {
throw new Error('Request queue full');
}
return new Promise((resolve, reject) => {
this.queue.push({ requestFn, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
while (this.queue.length > 0) {
const { requestFn, resolve, reject } = this.queue.shift();
try {
const result = await requestFn();
resolve(result);
// Check remaining quota
if (result.data && result.data.remainingRequests < 10) {
console.warn('Low quota - pausing queue processing');
break;
}
} catch (error) {
reject(error);
// Stop processing on rate limit
if (error.status === 429) {
console.error('Rate limited - pausing queue');
break;
}
}
// Add delay between requests to be respectful
await new Promise(resolve => setTimeout(resolve, 100));
}
this.processing = false;
}
}
// Usage
const queue = new RequestQueue();
async function screenPerson(name) {
return queue.enqueue(async () => {
const response = await fetch('https://api.matchstra.ca/api/Screening/screen', {
method: 'POST',
headers: {
'X-API-Key': process.env.MATCHSTRA_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name, entityType: 'Individual' })
});
return response.json();
});
}
3. Cache Results
Avoid redundant API calls by caching results:
class ResultCache {
constructor(ttlMinutes = 60) {
this.cache = new Map();
this.ttl = ttlMinutes * 60 * 1000; // Convert to ms
}
getCacheKey(name, entityType, dateOfBirth) {
return `${name}:${entityType}:${dateOfBirth || ''}`.toLowerCase();
}
get(name, entityType, dateOfBirth) {
const key = this.getCacheKey(name, entityType, dateOfBirth);
const cached = this.cache.get(key);
if (!cached) return null;
// Check if expired
if (Date.now() - cached.timestamp > this.ttl) {
this.cache.delete(key);
return null;
}
console.log(`Cache hit for ${name}`);
return cached.data;
}
set(name, entityType, dateOfBirth, data) {
const key = this.getCacheKey(name, entityType, dateOfBirth);
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
}
// Usage
const cache = new ResultCache(60); // 60 minute TTL
async function screenWithCache(name, entityType, dateOfBirth) {
// Check cache first
const cached = cache.get(name, entityType, dateOfBirth);
if (cached) {
return cached;
}
// Make API call
const response = await fetch('https://api.matchstra.ca/api/Screening/screen', {
method: 'POST',
headers: {
'X-API-Key': process.env.MATCHSTRA_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name, entityType, dateOfBirth })
});
const data = await response.json();
// Cache result
if (data.success) {
cache.set(name, entityType, dateOfBirth, data);
}
return data;
}
4. Batch Processing
For bulk operations, spread them over time:
async function processBatch(items, batchSize = 10, delayMs = 1000) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
console.log(`Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(items.length / batchSize)}`);
const batchResults = await Promise.all(
batch.map(item => screenPerson(item.name, item.entityType))
);
results.push(...batchResults);
// Check if we should continue
const lastResult = batchResults[batchResults.length - 1];
if (lastResult?.data?.remainingRequests < batchSize) {
console.warn('Approaching quota limit - stopping batch processing');
break;
}
// Delay between batches
if (i + batchSize < items.length) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
return results;
}
// Usage
const items = [
{ name: 'John Doe', entityType: 'Individual' },
{ name: 'Jane Smith', entityType: 'Individual' },
// ... 100 more items
];
const results = await processBatch(items, 10, 1000); // 10 at a time, 1s delay
5. Optimize API Usage
Reduce unnecessary calls:
// ❌ Bad: Multiple redundant calls
for (const user of users) {
await screenPerson(user.name);
await screenPerson(user.name); // Duplicate!
}
// ✅ Good: Deduplicate first
const uniqueNames = [...new Set(users.map(u => u.name))];
for (const name of uniqueNames) {
await screenPerson(name);
}
// ❌ Bad: Screening every keystroke
input.addEventListener('input', async (e) => {
await screenPerson(e.target.value); // Too many calls!
});
// ✅ Good: Debounce user input
let debounceTimer;
input.addEventListener('input', (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
await screenPerson(e.target.value);
}, 500); // Wait 500ms after user stops typing
});
Quota Management Dashboard
Build a simple dashboard to track usage:
class QuotaDashboard {
constructor() {
this.stats = {
totalCalls: 0,
successfulCalls: 0,
failedCalls: 0,
rateLimitErrors: 0,
lastReset: Date.now()
};
}
recordCall(success, isRateLimitError = false) {
this.stats.totalCalls++;
if (success) {
this.stats.successfulCalls++;
} else {
this.stats.failedCalls++;
if (isRateLimitError) {
this.stats.rateLimitErrors++;
}
}
}
getStats() {
return {
...this.stats,
successRate: (this.stats.successfulCalls / this.stats.totalCalls * 100).toFixed(2) + '%',
uptime: Date.now() - this.stats.lastReset
};
}
displayDashboard() {
const stats = this.getStats();
console.table(stats);
}
}
// Usage
const dashboard = new QuotaDashboard();
async function makeTrackedAPICall(url, options) {
try {
const response = await fetch(url, options);
const data = await response.json();
dashboard.recordCall(data.success, response.status === 429);
return data;
} catch (error) {
dashboard.recordCall(false);
throw error;
}
}
// Display stats periodically
setInterval(() => {
dashboard.displayDashboard();
}, 60000); // Every minute
Upgrading Your Plan
When you consistently hit quota limits:
- Review usage patterns - Identify optimization opportunities
- Implement caching - Reduce redundant calls
- Consider upgrade - Move to a higher-tier plan
Upgrade process:
- Log in to your Matchstra Dashboard
- Navigate to Billing → Plans
- Select a higher-tier plan
- Complete payment
- Quota increases immediately
Overage Policies
Some plans allow overages (extra requests beyond quota):
Example:
- Base quota: 5,000 requests/month
- Overage rate: $0.10/request
- Usage: 5,250 requests
- Overage charge: 250 × $0.10 = $25.00
Not all plans support overages. Starter plans typically hard-cap at the quota limit, resulting in 429 errors.
FAQ
Can I purchase additional quota mid-cycle?
Yes, you can upgrade your plan at any time. The new quota takes effect immediately.
Do failed requests count toward my quota?
No, only successful requests (HTTP 200) that return data count toward your quota. Authentication failures (401), validation errors (400), and server errors (500) do not count.
Does the API Explorer count toward my quota?
Yes, all requests made through the API Explorer use your production API key and count toward your quota.
Can I have different quotas for different environments?
Yes, generate separate API keys for production, staging, and development. Each key can be on a different plan with its own quota.
What happens at quota renewal?
Your quota resets at the start of each billing period (typically monthly). The exact reset date is shown in your dashboard.
Next Steps
- Monitor your quota in the Matchstra Dashboard
- Review Error Handling to gracefully handle 429 errors
- Optimize your integration to reduce unnecessary calls
- Contact Sales to discuss enterprise plans with custom quotas