Rate limit errors, duplicate rows, and timezone drift are three separate bugs with three separate fixes, even though they usually get lumped together as "the sync is broken." 429 errors are almost always solved with exponential backoff plus request batching, not a bigger quota. Duplicates are almost always solved by switching from an append action to an upsert keyed on a genuinely unique identifier, not by manually deleting rows every week. Timezone drift is almost always solved by normalizing every timestamp to UTC before it touches either system — a step most general setup guides skip entirely. Below: how to diagnose which one you actually have, how to fix it, and how to keep it from quietly coming back.
| Symptom | Likely cause | Jump to |
|---|---|---|
429 / "Too Many Requests" in your script log or automation history | Sheets API or CRM API rate limit hit | Rate limits ↓ |
| A new contact/record is created every run for the same person | Append-only write action, no unique-key match | Duplicates ↓ |
| The same row appears 2-3× after a retry or a resent webhook | No idempotency check on retried events | Duplicates ↓ |
| Dates shift by several hours or land on the wrong calendar day | Local spreadsheet time written straight into a UTC-expecting field | Timezone drift ↓ |
Why These Three Failures Keep Coming Back
A lot of Sheets-to-CRM syncs get built the same way: a spreadsheet is already the source of truth for a lead list, a signup form, or an ops tracker, and someone wires it to a CRM with Apps Script, a Zap, or a small scheduled job because the volume doesn't justify a dedicated integration. That setup works fine for months — and then contact volume crosses a few hundred rows, a second team starts writing to the same sheet, or a webhook starts retrying under load, and the exact same code starts throwing errors it never threw before.
None of that is really a "Google Sheets is broken" problem. It's three narrow, well-documented limits and behaviors that only bite once volume, concurrency, or geography changes: API rate ceilings, weak matching logic, and date values that don't carry timezone information the way people assume. Each one below has a distinct fix, and diagnosing the wrong one wastes more time than the actual repair does.
Rate Limit Errors (429s)
A 429 can come from two different places, and the fix depends on which one it is: the Google Sheets API itself, or the CRM's own API on the other end of the sync.
| Limit type | Cap |
|---|---|
| Read requests, per project | 300 / minute |
| Write requests, per project | 300 / minute |
| Read requests, per user per project | 60 / minute |
| Write requests, per user per project | 60 / minute |
If several scripts or service accounts hit the same Google Cloud project, the 300/minute project-wide ceiling can trip even when no single user goes over 60/minute — worth checking before you assume it's one runaway script. If your sync runs on Apps Script rather than calling the API directly, it's also bound by its own, separate set of daily quotas that stack independently of the Sheets API limits above:
| Limit type | Consumer account | Workspace account |
|---|---|---|
| URL Fetch calls | 20,000 / day | 100,000 / day |
| Trigger total runtime | 90 min / day | 6 hr / day |
| Max execution time per run | 6 min / execution | |
On the CRM side, limits vary by vendor and plan. HubSpot private apps on Free/Starter get 100 requests per 10 seconds; Professional and Enterprise get 190 per 10 seconds; the API Limit Increase add-on raises any tier to 250 per 10 seconds, with daily caps ranging from 250,000 up to 1,000,000+ requests depending on tier — and a 429 response includes a policyName field that tells you whether you hit the 10-second window or the daily one. Pipedrive moved to a token-budget model: 30,000 base tokens × a plan multiplier (1× Lite, 2× Growth, 5× Premium, 7× Ultimate) × seat count, where a lightweight read might cost 2 tokens and a search can cost 40, plus a rolling 2-second burst limit on top — once the daily budget is spent, every request gets a 429 until the next reset. Salesforce works differently again: Enterprise Edition orgs get a rolling 24-hour budget of 100,000 API requests plus 1,000 more per user license — a 15-license org gets roughly 115,000 requests/day — shared across REST, SOAP, Bulk, and Connect APIs combined. Because the window rolls rather than resetting at a fixed midnight, a burst that trips the limit can keep blocking new calls for several hours until enough of the prior day's traffic ages out.
Diagnosing which limit you actually hit
Check the Apps Script execution log or the raw HTTP response body for the exact error text — "Quota exceeded for quota metric" points to Sheets, while a HubSpot response carrying a policyName or Pipedrive response counting down a token balance points to the CRM. If you're inside a no-code tool like Zapier or Make, a rate limit often doesn't surface as a visible error at all — the step just stops running silently after a retry budget is exhausted, which is a different debugging path; this walkthrough on diagnosing Zaps that fail silently covers that presentation specifically.
The fix
- 1Exponential backoff with jitter. Google's own recommended formula is
min((2^n) + random_ms, max_backoff), starting around 1 second and capping around 32-64 seconds. This alone resolves most occasional 429s without any architecture change. - 2Batch instead of looping row by row.
spreadsheets.values.batchGetandbatchUpdatecount as a single call against quota no matter how many ranges they cover — replacing dozens of per-row calls with one batch call is usually the single biggest quota win available. - 3Cache reads with a TTL. If a range hasn't changed since the last run, don't re-read it. A short cache window removes most redundant read traffic without adding real staleness.
- 4Only request a quota increase after the above. Google's Cloud Console quota-increase flow expects to see efficient usage already in place — raising a request before batching and backoff are implemented just delays hitting the same wall at a slightly higher volume.
Duplicate Rows and Duplicate Contacts
Duplicates almost always trace back to one of three specific setup mistakes, not to Sheets or the CRM "glitching":
- An append-only write action. Many syncs are built with a "create new row" or "create new contact" action rather than a "find or create" (upsert) action, so every run re-adds records that already exist.
- A weak or missing matching key. Even when upsert is configured, matching on a non-unique field — a name or a company — lets near-duplicates through: case differences, trailing whitespace, or "Bob" vs. "Robert" all read as a new person to a naive match.
- Retries and race conditions. A webhook-triggered sync that retries on timeout without confirming the original request actually succeeded can process the same event twice; two triggers firing on overlapping schedules can do the same thing to the same rows.
The fix
- 1Switch the write action from "create" to "find or create." Nearly every native connector and automation platform supports this; it's frequently left on the default "create" action because that's what a first-time setup wizard offers.
- 2Pick one canonical match key. Email address or a CRM-native record ID — never a name or company field, which are not guaranteed unique in either system.
- 3Write the CRM's record ID back into the sheet after the first sync. Every later run should look up by that ID directly instead of re-running a fuzzy match against email or name.
- 4Add an idempotency key. A hash of the row's content, or the webhook's own event ID, checked against a small log before processing — so a retried event is a no-op the second time it arrives.
- 5Lock against overlapping runs. If two triggers can fire concurrently, a simple "sync in progress" flag prevents both from processing the same batch of rows at once.
Identity matching gets harder once a third system enters the picture — the same class of problem shows up, for instance, when syncing HubSpot with an accounting tool, where contact records and company records can be matched against the wrong entity entirely. And if duplicate contacts are showing up specifically through a Slack-triggered path rather than a Sheets one, this breakdown of Slack-HubSpot duplicate contacts covers the same root causes from that angle.
Timezone and Date Drift
This is the failure mode most general sync guides don't mention at all — including setup guides for this exact pairing — but it generates just as much cleanup work as the other two, and it's quieter, because nothing throws an error.
Google Sheets stores every date and time as a serial number: whole days since December 30, 1899, with the time of day as a decimal fraction of one day. Each spreadsheet also carries its own timezone in its file settings — but that per-file timezone only controls how Sheets displays the serial number in the UI. It doesn't travel with the value when a script or API call reads that cell, and most CRMs store and expect timestamps in UTC. A script that reads a Sheets date and writes it straight into a CRM date field without an explicit conversion will drift by however many hours separate the CRM's UTC baseline from the spreadsheet's actual local timezone.
There's a second subtlety worth a one-time check: if a spreadsheet's file-level timezone was changed after it was created — common when a template gets duplicated across regional teams — previously synced values don't retroactively change meaning, but new reads of the same unchanged cell can resolve to a different wall-clock time than before. If a sheet has ever been duplicated or handed to a different team, it's worth confirming its file timezone actually matches what your team assumes.
The fix
Normalize at the boundary, not in the display layer. Writing from Sheets into a CRM: convert explicitly to ISO 8601 UTC before sending — in Apps Script, that means something like Utilities.formatDate(date, 'Etc/GMT', "yyyy-MM-dd'T'HH:mm:ss'Z'") rather than trusting the cell's default formatted string. Writing from a CRM back into Sheets for reporting: apply the same explicit conversion on the way in, and set the sheet's file timezone to match the timezone your team actually reads the report in, rather than whatever it defaulted to when the file was created.
Building a Sync That Doesn't Break Again
Fixing the three failures above stops the bleeding. Keeping them fixed needs a small amount of ongoing visibility that most hand-built syncs skip entirely:
- Log every write with its idempotency key and a timestamp — even a plain "sync log" tab turns "why do we have duplicates" into a five-minute lookup instead of a forensic exercise.
- Alert on the 429 rate, not just on hard failures. A sync that logs occasional 429s and recovers via backoff is healthy; one where that rate is climbing week over week is heading toward a wall worth fixing before it becomes an outage.
- Run a periodic reconciliation job that counts records on both sides and flags a delta past a small threshold — "no error in the log" is not the same guarantee as "the data matches."
- Give retries a dead-letter path. After a set number of backoff attempts, write the failed row to a separate "needs attention" tab instead of silently dropping it or retrying forever. This single gap is the most common one we see in hand-built syncs.
When to Stop Patching and Replace the Sync
If you're layering all four fixes above onto a single Apps Script file or one Zap and it's still fragile, that's usually a sign the integration has outgrown a spreadsheet-based sync rather than a sign you're missing one more patch. If you haven't set up the sync yet or want to compare setup approaches from scratch, our guide to syncing Google Sheets with a CRM walks through the native, automation-platform, and API methods. And if the real question is whether to move off a script-based approach entirely, this decision framework for native integrations vs. iPaaS vs. custom API covers how to make that call.
Frequently Asked Questions
Why does my Google Sheets to CRM sync work fine for weeks and then suddenly start failing?
Is requesting a higher Google Sheets API quota worth it?
How do I find duplicate contacts that already exist in my CRM from a broken sync?
Do I need to convert timezones if my team and my CRM are in the same timezone?
Does batching requests actually help if my sync is still hitting per-user limits?
batchUpdate call counts as one.Methodology
This guide is based on Google's own Sheets API and Apps Script documentation, the current published rate-limit and quota pages for HubSpot, Pipedrive, and Salesforce, and patterns repeated across public troubleshooting threads from teams running their own Sheets-to-CRM sync. Every quota and rate-limit figure above is quoted from the vendor's own current documentation, checked in September 2026, rather than from community estimates.
References & Sources
Sources checked and cited directly in this guide:
- Google — Sheets API: Usage limits
- Google — Apps Script: Quotas for services
- HubSpot Developers — API usage guidelines and rate limits
- Pipedrive Developers — Rate limiting
- Salesforce Developers — API Request Limits and Allocations
- Google Docs Editors Community — Google Sheets rate limits creating enrolment errors with HubSpot CRM