hubspot-cost-tuning
Installation
SKILL.md
HubSpot Cost Tuning
Overview
Optimize HubSpot integration costs by reducing API call volume, monitoring usage against daily limits, and choosing the right plan.
Prerequisites
- Access to HubSpot account settings (Settings > Account > Usage & Limits)
- Understanding of current API usage patterns
Instructions
Step 1: Understand HubSpot API Pricing Model
HubSpot API calls are included with your subscription tier. There is no per-call billing, but exceeding limits results in 429 Too Many Requests errors that block your integration.
| Plan | Daily API Limit | Per-Second Limit |
|---|---|---|
| Free / Starter | 250,000 | 10 |
| Professional | 500,000 | 10 |
| Enterprise | 500,000 | 10 |
| API Limit Increase Add-on | 1,000,000 | 10 |
Key insight: The daily limit is per portal (shared across all apps). A poorly written integration can consume the entire quota and block all other apps.
Step 2: Monitor Current Usage
# Check rate limit headers on any API call
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: 487234
# X-HubSpot-RateLimit-Secondly: 10
# X-HubSpot-RateLimit-Secondly-Remaining: 9
// Programmatic usage tracking
class HubSpotUsageTracker {
private dailyCalls = 0;
private lastReset = new Date();
track(): void {
this.dailyCalls++;
// Reset counter at midnight
const now = new Date();
if (now.getDate() !== this.lastReset.getDate()) {
this.dailyCalls = 0;
this.lastReset = now;
}
}
getUsage(): { daily: number; percentUsed: number } {
const limit = parseInt(process.env.HUBSPOT_DAILY_LIMIT || '500000');
return {
daily: this.dailyCalls,
percentUsed: (this.dailyCalls / limit) * 100,
};
}
shouldAlert(): boolean {
return this.getUsage().percentUsed > 80;
}
}
Step 3: High-Impact Cost Reductions
Replace Individual Reads with Batch Reads
// BEFORE: 100 API calls
for (const id of contactIds) {
await client.crm.contacts.basicApi.getById(id, ['email']);
}
// AFTER: 1 API call (100x reduction)
await client.crm.contacts.batchApi.read({
inputs: contactIds.map(id => ({ id })),
properties: ['email'],
propertiesWithHistory: [],
});
Use Search Instead of List + Filter
// BEFORE: Fetch all, filter in memory (wastes API calls + bandwidth)
let after: string | undefined;
const matches = [];
do {
const page = await client.crm.contacts.basicApi.getPage(100, after, ['lifecyclestage']);
matches.push(...page.results.filter(c => c.properties.lifecyclestage === 'customer'));
after = page.paging?.next?.after;
} while (after); // Could be hundreds of pages
// AFTER: 1 search call with server-side filtering
const results = await client.crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [{ propertyName: 'lifecyclestage', operator: 'EQ', value: 'customer' }],
}],
properties: ['email', 'firstname'],
limit: 100, after: 0, sorts: [],
});
Cache Pipeline and Property Metadata
// Pipelines and properties change rarely -- cache for 1 hour
// This saves 2 API calls per deal creation if you look up stage IDs
// BEFORE: 2 calls every time
const pipelines = await client.crm.pipelines.pipelinesApi.getAll('deals');
const properties = await client.crm.properties.coreApi.getAll('deals');
// AFTER: 2 calls per hour (from cache)
const pipelines = await getCachedPipelines('deals'); // see performance-tuning skill
Use Webhooks Instead of Polling
// BEFORE: Poll for changes every 60 seconds (1,440 calls/day)
setInterval(async () => {
const recent = await client.crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [{
propertyName: 'lastmodifieddate',
operator: 'GTE',
value: String(Date.now() - 60000),
}],
}],
properties: ['email'], limit: 100, after: 0, sorts: [],
});
processChanges(recent.results);
}, 60000);
// AFTER: 0 polling calls (HubSpot pushes changes to you)
// Set up webhook subscription for contact.propertyChange
// See hubspot-webhooks-events skill
Step 4: Usage Dashboard Query
-- Track API usage if you log calls to a database
SELECT
DATE_TRUNC('hour', called_at) as hour,
endpoint,
COUNT(*) as calls,
COUNT(*) FILTER (WHERE status_code = 429) as rate_limited,
AVG(response_ms) as avg_latency_ms
FROM hubspot_api_log
WHERE called_at >= NOW() - INTERVAL '24 hours'
GROUP BY 1, 2
ORDER BY calls DESC;
Output
- API call volume tracked and monitored
- Batch operations replacing individual calls (100x reduction)
- Search replacing list+filter patterns
- Webhooks replacing polling (1,440 calls/day saved)
- Metadata cached to avoid redundant lookups
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| Daily limit hit | Unoptimized code | Apply batch + cache + webhook patterns |
| All apps blocked | Shared portal limit | Identify heaviest caller, optimize |
| No visibility | No tracking | Add usage counter middleware |
| Sudden spike | New feature deployed | Review new code for N+1 patterns |
Resources
Next Steps
For architecture patterns, see hubspot-reference-architecture.
Weekly Installs
1
Repository
jeremylongshore…s-skillsGitHub Stars
2.1K
First Seen
Mar 25, 2026
Security Audits