From hubspot-pack
Implements HubSpot API rate limiting with SDK retries, exponential backoff, jitter, and Retry-After handling for 429 errors and throughput optimization.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin hubspot-packThis skill is limited to using the following tools:
Handle HubSpot API rate limits with proper backoff strategies. HubSpot enforces per-second and daily limits shared across all apps in a portal.
Optimizes HubSpot API costs by monitoring usage against daily limits, selecting plans, and reducing calls via batch reads and tracking.
Provides expert patterns for HubSpot CRM integration including OAuth authentication, CRM objects, associations, batch operations, webhooks, and custom objects using Node.js and Python SDKs.
Handles Intercom API rate limits with exponential backoff, header monitoring, and retry logic for 429 errors and throughput optimization.
Share bugs, ideas, or general feedback.
Handle HubSpot API rate limits with proper backoff strategies. HubSpot enforces per-second and daily limits shared across all apps in a portal.
@hubspot/api-client installed| Plan | Per-Second Limit | Daily Limit | Burst |
|---|---|---|---|
| Free/Starter | 10 requests/sec | 250,000/day | -- |
| Professional | 10 requests/sec | 500,000/day | -- |
| Enterprise | 10 requests/sec | 500,000/day | -- |
| API Add-on | 10 requests/sec | 1,000,000/day | -- |
Critical: Limits are per HubSpot portal (account), not per app. All private apps and OAuth apps in the same portal share the same limit bucket.
import * as hubspot from '@hubspot/api-client';
// The SDK has built-in retry for 429 responses
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
numberOfApiCallRetries: 3, // retries 429 and 5xx automatically
});
async function withHubSpotBackoff<T>(
operation: () => Promise<T>,
config = { maxRetries: 5, baseDelayMs: 1000, maxDelayMs: 30000 }
): Promise<T> {
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await operation();
} catch (error: any) {
if (attempt === config.maxRetries) throw error;
const status = error?.code || error?.statusCode || error?.response?.status;
// Only retry on 429 and 5xx
if (status !== 429 && (status < 500 || status >= 600)) throw error;
// Honor Retry-After header from HubSpot
let delay: number;
const retryAfter = error?.response?.headers?.['retry-after'];
if (retryAfter) {
delay = parseInt(retryAfter) * 1000;
} else {
// Exponential backoff with jitter
const exponential = config.baseDelayMs * Math.pow(2, attempt);
const jitter = Math.random() * 500;
delay = Math.min(exponential + jitter, config.maxDelayMs);
}
console.warn(`HubSpot rate limited (attempt ${attempt + 1}/${config.maxRetries}). ` +
`Retrying in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error('Unreachable');
}
import PQueue from 'p-queue';
// Queue that respects HubSpot's 10 req/sec limit
const hubspotQueue = new PQueue({
concurrency: 5, // max parallel requests
interval: 1000, // per second
intervalCap: 10, // max 10 per interval (HubSpot limit)
});
async function queuedRequest<T>(operation: () => Promise<T>): Promise<T> {
return hubspotQueue.add(operation) as Promise<T>;
}
// Usage -- all calls automatically throttled
const results = await Promise.all(
contactIds.map(id =>
queuedRequest(() =>
client.crm.contacts.basicApi.getById(id, ['email', 'firstname'])
)
)
);
// Instead of 100 individual GET calls (100 API calls):
// BAD
for (const id of contactIds) {
await client.crm.contacts.basicApi.getById(id, ['email']);
}
// Use batch read (1 API call for up to 100 records):
// GOOD - POST /crm/v3/objects/contacts/batch/read
const batchResult = await client.crm.contacts.batchApi.read({
inputs: contactIds.map(id => ({ id })),
properties: ['email', 'firstname', 'lastname'],
propertiesWithHistory: [],
});
console.log(`Fetched ${batchResult.results.length} contacts in 1 API call`);
class HubSpotRateLimitMonitor {
private dailyRemaining = 500000;
private secondlyRemaining = 10;
updateFromResponse(headers: Record<string, string>): void {
if (headers['x-hubspot-ratelimit-daily-remaining']) {
this.dailyRemaining = parseInt(headers['x-hubspot-ratelimit-daily-remaining']);
}
if (headers['x-hubspot-ratelimit-secondly-remaining']) {
this.secondlyRemaining = parseInt(headers['x-hubspot-ratelimit-secondly-remaining']);
}
}
shouldThrottle(): boolean {
return this.secondlyRemaining < 2 || this.dailyRemaining < 1000;
}
getStatus(): { daily: number; secondly: number; warning: boolean } {
return {
daily: this.dailyRemaining,
secondly: this.secondlyRemaining,
warning: this.shouldThrottle(),
};
}
}
Retry-After header| Header | Description | Action |
|---|---|---|
X-HubSpot-RateLimit-Daily | Daily quota | Monitor usage |
X-HubSpot-RateLimit-Daily-Remaining | Remaining today | Alert if < 10% |
X-HubSpot-RateLimit-Secondly | Per-second limit | Always 10 |
X-HubSpot-RateLimit-Secondly-Remaining | Remaining this second | Throttle if < 2 |
Retry-After | Seconds to wait | Always honor this |
# Check current rate limit state
curl -sI https://api.hubapi.com/crm/v3/objects/contacts?limit=1 \
-H "Authorization: Bearer $HUBSPOT_ACCESS_TOKEN" \
| grep -i ratelimit
# Output:
# X-HubSpot-RateLimit-Daily: 500000
# X-HubSpot-RateLimit-Daily-Remaining: 499800
# X-HubSpot-RateLimit-Secondly: 10
# X-HubSpot-RateLimit-Secondly-Remaining: 9
For security configuration, see hubspot-security-basics.