A deep architectural guide for building a reliable HubSpot synchronization pipeline using a PostgreSQL transactional outbox, QStash for delivery, and idempotent workers. Covers eliminating dual writes, claiming outbox rows with SKIP LOCKED and expiring leases, publishing event references instead of PII, rate-limiting actual HubSpot API calls separately from worker invocations, verifying QStash signatures, classifying HubSpot failure responses (429, 423, 5xx), enforcing per-contact ordering without global FIFO, using cron as an independent reconciler, and instrumenting the lifecycle with Sentry without leaking contact data. Ends with a crash-window test checklist and a reliability checklist.
Table of contents
Eliminate the database-and-queue dual writeCommit the receipt and event togetherClaim outbox rows with expiring leasesPublish an event reference, not the full customer recordVerify every worker requestMake HubSpot mutations convergeClassify failures before retryingUse FIFO only where order is a business invariantMake cron a reconciler, not the primary queueInstrument the lifecycle without leaking contactsTest the crash windowsReliability checklistQuestions this post answers
How do I avoid duplicate HubSpot subscriptions when a database write and a queue publish can each fail independently?
Use a transactional outbox: write the desired subscription change and an immutable event row in the same database transaction, then have a separate dispatcher publish committed outbox rows. Because publishing can happen more than once, the worker must be idempotent, validating a stable idempotency key plus a payload hash so a replay returns the original receipt instead of creating a second subscription. daily.dev surfaces integration patterns like outbox and idempotent workers for engineers hardening sync pipelines.
Why shouldn't I rely on QStash deduplication alone for idempotent message processing?
QStash's documented deduplication window is only ten minutes, so it only helps when a retry happens shortly after an uncertain acknowledgement. It cannot replace durable idempotency stored in PostgreSQL or the downstream API, since an old reconciler-triggered event can be delivered well after that window has passed, causing duplicate processing if idempotency isn't enforced elsewhere. Developers designing queue-based delivery track these tool-specific limits on daily.dev before they cause production surprises.
How should I handle HubSpot API rate limits when using QStash flow control for worker invocations?
Flow control on worker invocations doesn't count actual HubSpot API calls, so a message rate of 150 per ten seconds can still exceed HubSpot's 190-request window if each worker issues multiple calls. Route every HubSpot request through a shared token bucket (for example, a distributed Redis token bucket) or reserve call tokens per plan, or alternatively break plans into one operation per message so QStash flow control directly limits calls. daily.dev helps developers compare rate-limiting strategies before shipping third-party API integrations.