From hubspot-pack
Implement HubSpot reliability patterns: circuit breakers, retries, and graceful degradation. Use when building fault-tolerant HubSpot integrations, implementing retry strategies, or adding resilience to production CRM services. Trigger with phrases like "hubspot reliability", "hubspot circuit breaker", "hubspot resilience", "hubspot fallback", "hubspot fault tolerant".
npx claudepluginhub flight505/skill-forge --plugin hubspot-packThis skill is limited to using the following tools:
Production-grade reliability patterns for HubSpot CRM integrations: circuit breaker, retry with Retry-After, graceful degradation, and dead letter queues.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Share bugs, ideas, or general feedback.
Production-grade reliability patterns for HubSpot CRM integrations: circuit breaker, retry with Retry-After, graceful degradation, and dead letter queues.
@hubspot/api-client installed (has built-in retry)opossum for circuit breakerimport * as hubspot from '@hubspot/api-client';
// The SDK automatically retries 429 and 5xx errors
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
numberOfApiCallRetries: 3, // retries with exponential backoff
});
// This handles most transient failures automatically
import CircuitBreaker from 'opossum';
// Circuit breaker wrapping all HubSpot API calls
const hubspotBreaker = new CircuitBreaker(
async <T>(operation: () => Promise<T>): Promise<T> => operation(),
{
timeout: 15000, // 15s timeout per call
errorThresholdPercentage: 50, // open after 50% failure rate
resetTimeout: 30000, // try again after 30s
volumeThreshold: 5, // need 5+ calls before evaluating
rollingCountTimeout: 60000, // 60s rolling window
}
);
// Monitor circuit state
hubspotBreaker.on('open', () => {
console.warn('[HubSpot] Circuit OPEN -- requests failing fast');
// Alert team: HubSpot integration degraded
});
hubspotBreaker.on('halfOpen', () => {
console.info('[HubSpot] Circuit HALF-OPEN -- testing recovery');
});
hubspotBreaker.on('close', () => {
console.info('[HubSpot] Circuit CLOSED -- normal operation');
});
// Usage
async function resilientHubSpotCall<T>(operation: () => Promise<T>): Promise<T> {
return hubspotBreaker.fire(operation) as Promise<T>;
}
// Example
const contacts = await resilientHubSpotCall(() =>
client.crm.contacts.basicApi.getPage(10, undefined, ['email'])
);
// Serve cached/fallback data when HubSpot is unavailable
import { LRUCache } from 'lru-cache';
const fallbackCache = new LRUCache<string, any>({
max: 10000,
ttl: 30 * 60 * 1000, // 30 minutes
});
async function withFallback<T>(
cacheKey: string,
operation: () => Promise<T>,
fallback?: T
): Promise<{ data: T; source: 'live' | 'cache' | 'fallback' }> {
try {
const data = await resilientHubSpotCall(operation);
fallbackCache.set(cacheKey, data);
return { data, source: 'live' };
} catch (error) {
// Try cache first
const cached = fallbackCache.get(cacheKey);
if (cached) {
console.warn(`[HubSpot] Serving cached data for ${cacheKey}`);
return { data: cached as T, source: 'cache' };
}
// Use static fallback if provided
if (fallback !== undefined) {
console.warn(`[HubSpot] Serving fallback for ${cacheKey}`);
return { data: fallback, source: 'fallback' };
}
throw error;
}
}
// Usage
const { data: contacts, source } = await withFallback(
'recent-contacts',
() => client.crm.contacts.basicApi.getPage(10, undefined, ['email', 'firstname']),
{ results: [], paging: undefined } // empty fallback
);
if (source !== 'live') {
console.warn(`Serving ${source} data -- HubSpot may be degraded`);
}
interface FailedOperation {
id: string;
operation: string;
payload: any;
error: string;
correlationId?: string;
attempts: number;
firstAttempt: string;
lastAttempt: string;
}
class HubSpotDeadLetterQueue {
private queue: FailedOperation[] = [];
add(operation: string, payload: any, error: any): void {
const entry: FailedOperation = {
id: `dlq-${Date.now()}-${Math.random().toString(36).slice(2)}`,
operation,
payload,
error: error?.body?.message || error.message,
correlationId: error?.body?.correlationId,
attempts: 1,
firstAttempt: new Date().toISOString(),
lastAttempt: new Date().toISOString(),
};
this.queue.push(entry);
console.warn(`[DLQ] Enqueued ${operation}: ${entry.error}`);
}
async retryAll(): Promise<{ succeeded: number; failed: number }> {
let succeeded = 0, failed = 0;
for (const entry of [...this.queue]) {
try {
// Retry the operation
await this.executeOperation(entry);
this.queue = this.queue.filter(e => e.id !== entry.id);
succeeded++;
} catch (error) {
entry.attempts++;
entry.lastAttempt = new Date().toISOString();
failed++;
if (entry.attempts > 5) {
console.error(`[DLQ] Giving up on ${entry.id} after ${entry.attempts} attempts`);
// Move to permanent failure storage
}
}
}
return { succeeded, failed };
}
private async executeOperation(entry: FailedOperation): Promise<void> {
switch (entry.operation) {
case 'createContact':
await client.crm.contacts.basicApi.create({
properties: entry.payload,
associations: [],
});
break;
case 'updateContact':
await client.crm.contacts.basicApi.update(
entry.payload.id, { properties: entry.payload.properties }
);
break;
// Add more operation types as needed
}
}
getStats(): { pending: number; oldestAge: string } {
return {
pending: this.queue.length,
oldestAge: this.queue.length > 0
? this.queue[0].firstAttempt
: 'none',
};
}
}
const dlq = new HubSpotDeadLetterQueue();
// Usage
async function createContactWithDLQ(properties: Record<string, string>) {
try {
return await resilientHubSpotCall(() =>
client.crm.contacts.basicApi.create({ properties, associations: [] })
);
} catch (error) {
dlq.add('createContact', properties, error);
throw error;
}
}
// Retry DLQ periodically
setInterval(async () => {
const stats = dlq.getStats();
if (stats.pending > 0) {
console.log(`[DLQ] Retrying ${stats.pending} failed operations...`);
const result = await dlq.retryAll();
console.log(`[DLQ] Results: ${result.succeeded} succeeded, ${result.failed} failed`);
}
}, 5 * 60 * 1000); // every 5 minutes
type HealthStatus = 'healthy' | 'degraded' | 'unhealthy';
async function hubspotHealthCheck(): Promise<{
status: HealthStatus;
circuitState: string;
dlqPending: number;
apiLatencyMs: number;
}> {
const dlqStats = dlq.getStats();
const start = Date.now();
try {
await client.crm.contacts.basicApi.getPage(1);
const latency = Date.now() - start;
const status: HealthStatus =
hubspotBreaker.stats().state === 'open' ? 'degraded' :
dlqStats.pending > 50 ? 'degraded' :
latency > 5000 ? 'degraded' :
'healthy';
return {
status,
circuitState: hubspotBreaker.stats().state,
dlqPending: dlqStats.pending,
apiLatencyMs: latency,
};
} catch {
return {
status: 'unhealthy',
circuitState: hubspotBreaker.stats().state,
dlqPending: dlqStats.pending,
apiLatencyMs: Date.now() - start,
};
}
}
| Issue | Cause | Solution |
|---|---|---|
| Circuit stays open | Threshold too low | Increase errorThresholdPercentage |
| Stale cache served | HubSpot down for long time | Alert when cache age > TTL |
| DLQ growing | Persistent failures | Investigate root cause, not symptoms |
| Fallback data confusing users | No degradation indicator | Show "data may be stale" in UI |
For policy enforcement, see hubspot-policy-guardrails.