Agent Skills: HubSpot Performance Tuning

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/hubspot-performance-tuning

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/hubspot-pack/skills/hubspot-performance-tuning

Skill Files

Browse the full folder contents for hubspot-performance-tuning.

Download Skill

Loading file tree…

plugins/saas-packs/hubspot-pack/skills/hubspot-performance-tuning/SKILL.md

Skill Metadata

Name
hubspot-performance-tuning
Description
|

HubSpot Performance Tuning

Overview

Optimize HubSpot API performance through batch operations, caching, search optimization, and request minimization.

Prerequisites

  • @hubspot/api-client installed
  • Understanding of your access patterns (read-heavy vs write-heavy)
  • Optional: Redis for distributed caching

Instructions

Step 1: Use Batch APIs Everywhere

The single biggest performance win: batch operations reduce API calls by up to 100x.

import * as hubspot from '@hubspot/api-client';

const client = new hubspot.Client({
  accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
  numberOfApiCallRetries: 3,
});

// BAD: 100 API calls for 100 contacts
async function getContactsSlow(ids: string[]) {
  return Promise.all(ids.map(id =>
    client.crm.contacts.basicApi.getById(id, ['email', 'firstname'])
  ));
}

// GOOD: 1 API call for 100 contacts
async function getContactsFast(ids: string[], properties: string[]) {
  // POST /crm/v3/objects/contacts/batch/read
  const result = await client.crm.contacts.batchApi.read({
    inputs: ids.map(id => ({ id })),
    properties,
    propertiesWithHistory: [],
  });
  return result.results;
}

// For more than 100 records, chunk:
async function getContactsChunked(ids: string[], properties: string[]) {
  const results = [];
  for (let i = 0; i < ids.length; i += 100) {
    const chunk = ids.slice(i, i + 100);
    const batch = await getContactsFast(chunk, properties);
    results.push(...batch);
  }
  return results;
}

Step 2: Request Only Needed Properties

// BAD: Returns ALL default properties (slow, large payload)
const contact = await client.crm.contacts.basicApi.getById(id);

// GOOD: Only request what you need (fast, small payload)
const contact = await client.crm.contacts.basicApi.getById(
  id,
  ['email', 'firstname', 'lastname', 'lifecyclestage'] // specific properties
);

Step 3: Cache Frequently Accessed Data

import { LRUCache } from 'lru-cache';

const contactCache = new LRUCache<string, any>({
  max: 5000,              // max entries
  ttl: 5 * 60 * 1000,     // 5 minute TTL
  updateAgeOnGet: true,
});

async function getCachedContact(id: string, properties: string[]) {
  const cacheKey = `contact:${id}`;
  const cached = contactCache.get(cacheKey);
  if (cached) return cached;

  const contact = await client.crm.contacts.basicApi.getById(id, properties);
  contactCache.set(cacheKey, contact);
  return contact;
}

// Invalidate on webhook events
function invalidateContactCache(contactId: string): void {
  contactCache.delete(`contact:${contactId}`);
}

Step 4: Optimize Search Queries

// BAD: Overly broad search, returns too much data
const bad = await client.crm.contacts.searchApi.doSearch({
  filterGroups: [],  // no filters = scan entire DB
  properties: [],    // returns default properties
  limit: 100,
  after: 0,
  sorts: [],
});

// GOOD: Specific filters, minimal properties, sorted
const good = await client.crm.contacts.searchApi.doSearch({
  filterGroups: [{
    filters: [
      { propertyName: 'lifecyclestage', operator: 'EQ', value: 'customer' },
      { propertyName: 'createdate', operator: 'GTE', value: String(Date.now() - 86400000) },
    ],
  }],
  properties: ['email', 'firstname'],  // only what you need
  limit: 50,
  after: 0,
  sorts: [{ propertyName: 'createdate', direction: 'DESCENDING' }],
});

// Search limits: max 5 filterGroups, 6 filters per group, 18 filters total
// Search returns max 10,000 results total (pagination limit)

Step 5: Pipeline and Property Caching

Pipeline stages and custom properties rarely change -- cache them aggressively:

let pipelineCache: Map<string, any> | null = null;
let pipelineCacheExpiry = 0;

async function getCachedPipelines(objectType: 'deals' | 'tickets') {
  if (pipelineCache && Date.now() < pipelineCacheExpiry) {
    return pipelineCache.get(objectType);
  }

  const pipelines = await client.crm.pipelines.pipelinesApi.getAll(objectType);
  if (!pipelineCache) pipelineCache = new Map();
  pipelineCache.set(objectType, pipelines.results);
  pipelineCacheExpiry = Date.now() + 3600000; // 1 hour cache
  return pipelines.results;
}

// Same pattern for properties
let propertyCache: Map<string, any> | null = null;

async function getCachedProperties(objectType: string) {
  if (propertyCache?.has(objectType)) {
    return propertyCache.get(objectType);
  }
  const props = await client.crm.properties.coreApi.getAll(objectType);
  if (!propertyCache) propertyCache = new Map();
  propertyCache.set(objectType, props.results);
  return props.results;
}

Step 6: Pagination with Async Generators

// Memory-efficient streaming for large exports
async function* streamAllContacts(properties: string[]) {
  let after: string | undefined;
  do {
    const page = await client.crm.contacts.basicApi.getPage(
      100,    // max page size
      after,
      properties
    );
    yield* page.results;
    after = page.paging?.next?.after;
  } while (after);
}

// Process millions of records without running out of memory
let count = 0;
for await (const contact of streamAllContacts(['email', 'firstname'])) {
  await processContact(contact);
  count++;
  if (count % 1000 === 0) console.log(`Processed ${count} contacts`);
}

Output

  • Batch operations reducing API calls by 100x
  • Property-specific requests reducing payload size
  • LRU caching for frequently accessed records
  • Optimized search queries with proper filters
  • Streaming pagination for large datasets

Error Handling

| Issue | Cause | Solution | |-------|-------|----------| | Batch returns 207 Multi-Status | Some records failed | Check individual status in response | | Cache stale data | TTL too long | Invalidate on webhook events | | Search returns max 10,000 | HubSpot search limit | Use getPage pagination instead | | Memory pressure | Caching too much | Set max entries on LRU cache |

Resources

Next Steps

For cost optimization, see hubspot-cost-tuning.