Skip to main content

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:

PlanMonthly QuotaOverages
Starter500 requestsNot available
Professional5,000 requests$0.10/request
Business25,000 requests$0.08/request
EnterpriseCustomCustom pricing
note

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:

  1. Review usage patterns - Identify optimization opportunities
  2. Implement caching - Reduce redundant calls
  3. Consider upgrade - Move to a higher-tier plan

Upgrade process:

  1. Log in to your Matchstra Dashboard
  2. Navigate to BillingPlans
  3. Select a higher-tier plan
  4. Complete payment
  5. 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
warning

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