Claude-code-plugins-plus-skills hubspot-reliability-patterns
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/hubspot-pack/skills/hubspot-reliability-patterns" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-hubspot-reliability-patterns && rm -rf "$T"
manifest:
plugins/saas-packs/hubspot-pack/skills/hubspot-reliability-patterns/SKILL.mdsource content
HubSpot Reliability Patterns
Overview
Production-grade reliability patterns for HubSpot CRM integrations: circuit breaker, retry with Retry-After, graceful degradation, and dead letter queues.
Prerequisites
installed (has built-in retry)@hubspot/api-client- Optional:
for circuit breakeropossum - Optional: Redis or database for dead letter queue
Instructions
Step 1: SDK Built-in Retry (First Line of Defense)
import * 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
Step 2: Circuit Breaker for HubSpot API
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']) );
Step 3: Graceful Degradation
// 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`); }
Step 4: Dead Letter Queue
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
Step 5: Health Check with Degraded State
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, }; } }
Output
- SDK built-in retry handling transient 429/5xx errors
- Circuit breaker preventing cascade failures
- Graceful degradation with cached/fallback data
- Dead letter queue for failed operations with automatic retry
- Health check reporting degraded state
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| Circuit stays open | Threshold too low | Increase |
| 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 |
Resources
Next Steps
For policy enforcement, see
hubspot-policy-guardrails.