[object Object]

The 1,000 property limit per object in HubSpot sounded like a number you could never hit. Then your portal is four years old, every campaign added “just one property,” every integration syncs “just a few fields,” and one morning the marketing team tries to add the 998th contact property and the UI throws a quota error. Suddenly the question is which 200 to delete by Friday because the segmentation campaign launches Monday.

Property quota is a slow-rolling crisis. Here is how to defuse it without losing the data you actually use.

Why portals hit the cap

Three mechanics, in order of severity.

  1. Integrations syncing wide tables: Salesforce mirror, marketing automation imports, custom apps pushing fields that nobody reviews
  2. Campaign-specific properties: “downloaded ebook X”, “attended webinar Y”, one per campaign, never cleaned up
  3. Calculated properties built for a single report that someone wanted once

Every one of those is created in seconds. None of them is reviewed in years.

Audit before you delete

Pull a full property usage report. HubSpot exposes population data per property: how many records have a non-null value. Sort ascending.

async function propertyUsageReport(objectType) {
  const props = await fetch(
    `https://api.hubapi.com/crm/v3/properties/${objectType}`,
    { headers: { Authorization: `Bearer ${TOKEN}` } },
  ).then((r) => r.json());

  const usage = [];
  for (const p of props.results) {
    const count = await countRecordsWithProperty(objectType, p.name);
    usage.push({
      name: p.name,
      label: p.label,
      type: p.type,
      groupName: p.groupName,
      createdAt: p.createdAt,
      modifiedAt: p.modifiedAt,
      hasValueCount: count,
      isCustom: !p.hubspotDefined,
    });
  }

  return usage.sort((a, b) => a.hasValueCount - b.hasValueCount);
}

The top of this report — properties with zero or near-zero population — is the cleanup queue. The bottom — heavily populated properties — is the keep-no-matter-what list.

The four-bucket triage

Sort every custom property into one of four buckets.

BucketRuleAction
Dead0% populated, not in any workflow or listArchive immediately
VestigialUnder 1% populated, no integration writesArchive after owner sign-off
NicheUnder 5% populated, used by a specific teamKeep, audit annually
CoreOver 5% populated, business-criticalKeep, document, never touch

The archive action in HubSpot hides the property from the UI without deleting data. If you change your mind, restore. Most cleanup uses archive.

The integration audit

Before you archive anything an integration touched, audit the integration mapping. An integration that writes to a property you archived will either silently fail or re-create the property under a new name. Both are bad.

For each integration:

  • Pull the mapping configuration
  • List the HubSpot properties it writes to
  • Cross-reference with the archive candidate list
  • Negotiate with the integration owner: either we map differently, or we keep the property

Common offenders: Salesforce sync mapping every Salesforce field, even unused ones. Marketing automation pushing every form field as a separate property when 80% should be consolidated.

Consolidate before you create

Before adding any property, check if an existing property covers the need. The most common waste pattern:

  • webinar_attended_q1_2024 (boolean, 12% populated)
  • webinar_attended_q2_2024 (boolean, 14% populated)
  • webinar_attended_q3_2024 (boolean, 11% populated)
  • webinar_attended_q4_2024 (boolean, 9% populated)

Four properties tracking the same concept. Should be one multi-select property webinars_attended with values per webinar. Or one date property last_webinar_attended_date with a related multi-select for which series.

// Migration: roll quarterly booleans into a single multi-select
async function migrateWebinarProps(contactId) {
  const c = await getContact(contactId, [
    "webinar_attended_q1_2024",
    "webinar_attended_q2_2024",
    "webinar_attended_q3_2024",
    "webinar_attended_q4_2024",
  ]);

  const attended = [];
  for (const q of ["q1", "q2", "q3", "q4"]) {
    if (c.properties[`webinar_attended_${q}_2024`] === "true") {
      attended.push(`${q}_2024`);
    }
  }

  await updateContact(contactId, {
    webinars_attended: attended.join(";"),
  });
}

Run the migration, validate the new property has the right values, then archive the four old ones. Four properties recovered.

The custom object escape hatch

When consolidation hits its limit and you genuinely need to track many distinct facts per contact, the answer is a custom object, not more contact properties.

Common signals you need a custom object:

  • You are tracking events with per-event metadata: date, source, value, status
  • Each contact has 0 to N of something, where N is more than 1
  • The data is queried frequently as a set, not individually

Examples that should be custom objects rather than property bloat:

  • Event registrations (event, date, status, attended)
  • Subscription history (plan, start, end, MRR)
  • Course completions (course, completed date, score)

See HubSpot custom objects guide for the implementation pattern.

Property groups are documentation

Properties without groups are properties that get orphaned. Force every custom property into a named group with an owner.

Group: Lead Source Detail   | Owner: Marketing Ops
Group: Sales Engagement     | Owner: Sales Ops
Group: Product Usage        | Owner: Product
Group: Compliance           | Owner: Legal
Group: Integration: SF      | Owner: RevOps
Group: Integration: Segment | Owner: Data

Quarterly, the owner reviews their group’s properties for archival candidates. The accountability is the system. Without it, nobody owns cleanup.

The “we will need it someday” trap

Properties created speculatively almost never get used. The intent was honest, the workflow never shipped, the property persists.

Default rule: no property is created without a documented use case and at least one workflow, list, or report planned within 30 days. If the planned use does not materialize in 60 days, archive.

Marketing teams hate this rule. Implement it anyway. The alternative is a quota crisis.

When you actually hit the cap

If you are reading this past 950 properties and you need 30 minutes of breathing room before the audit, the emergency tactic:

  1. Archive every property with createdAt over 18 months ago and hasValueCount of 0
  2. Archive every property in a “test” or “tmp” or “experiment” group
  3. Identify any property whose label includes a year that has passed and which has under 5% population, archive

This buys 50-150 properties of headroom on most portals. Use the breathing room to run the real audit.

Bottom line

  • Audit by population data; properties under 1% used are the cleanup queue.
  • Archive, do not delete; the rollback is the safety net.
  • Consolidate before you create; four quarterly booleans should be one multi-select.
  • Custom objects are the escape hatch when many facts per contact need first-class tracking.
  • Property groups with named owners are how cleanup becomes a recurring program, not a panic.
[object Object]
Share