All posts
·5 min read

HubSpot to internal DB sync: pitfalls we hit

Five painful lessons from building bi-directional HubSpot to Postgres syncs for SMB clients.

HubSpotsyncintegration

Two-way sync is harder than one-way

Pulling HubSpot into Postgres is easy. Pushing changes back without creating an infinite loop is hard. Here are the five things we got wrong on the first sync we built — so you do not have to.

1. The webhook → write → webhook loop

You receive a HubSpot webhook, you update Postgres. A trigger fires that updates HubSpot. HubSpot fires another webhook. Infinite loop.

Fix: tag every write you make to HubSpot with a custom property like internal_sync_at. When a webhook arrives, if the internal_sync_at is within the last 60 seconds, ignore it.

2. The conflict resolution problem

A user updates a contact in HubSpot. Your support tool updates the same contact two seconds later. Which one wins?

There is no universal answer. We default to "last write wins by timestamp" but expose this as a config the client can change per field. Email might be HubSpot-wins, lifecycle stage might be internal-wins.

Make this explicit. Do not hide it in code.

3. The deleted record problem

HubSpot's webhook for deletes is unreliable across edge cases (merges, GDPR deletes). We poll for deleted records every 6 hours as a backstop. Yes, it is ugly. Yes, it has saved us.

4. The rate limit

HubSpot's API rate limits are per-portal and they bite at scale. Use the batch endpoints (/crm/v3/objects/contacts/batch/read) for bulk syncs and respect the X-HubSpot-RateLimit-Remaining header.

If you are syncing more than 10k contacts, plan the initial backfill carefully — do it in chunks, off-hours, and instrument the retry path.

5. The custom property name change

A user renames a custom property in HubSpot. Your code references the old internal name. The sync silently breaks.

Fix: never reference custom property internal names in code. Store the mapping in your database, and reload it on a schedule. When the mapping changes, log it loudly.

What we always ship

Every HubSpot sync we build has:

  • Idempotent webhook handlers
  • A scheduled reconciliation job
  • A "force resync this contact" admin endpoint
  • Field-level mapping in config, not code
  • An audit log of every change with source and timestamp

These take an extra week to build. They save months of "why did this contact change?" support tickets.

Got a workflow problem?

Let's talk about whether n8n, a custom backend, or a hybrid fits your case.

A 30-minute discovery call. Free, honest, you leave with a written direction either way.

Start QuizBook a Call