QuickBooks API Rate Limits and a Slow Stripe Sync
A slow Stripe to QuickBooks sync is usually a queue, not a failure. What sits between a Stripe event and a QuickBooks record, which delays are designed,...
A charge went through in Stripe forty minutes ago. You open QuickBooks and it is not there. You open the Data Feed and the row exists, sitting on "in progress", carrying no error message, doing nothing visible.
The instinct at that moment is to treat the silence as a failure. Something must be stuck, so you hit resync, and when that does not help within a minute you hit it again on a few more rows.
That instinct is backwards. Almost everything between a Stripe event and a QuickBooks record is a queue, and a queue under pressure does not fail, it waits. The layer that decides how fast records land is a throttle, a token, and a set of flags whose entire job is to stop two workers writing the same record twice. None of them produce an error message, because none of them are errors.
This post is about that layer: what actually sits between "Stripe fired the event" and "QuickBooks has the record", which parts of the delay are designed, and why resyncing a slow row is usually the one action that makes it slower.
Want Stripe activity in QuickBooks without needing to know any of this? Start a free trial.
The path a charge takes, and where the time goes
A single Stripe charge crosses four boundaries before it becomes a QuickBooks record.
- Stripe emits the event and delivers it to a webhook endpoint.
- Acodei turns the event into a Transaction row, one per syncable Stripe object, carrying the event type, the extracted amounts, the currency, and the balance-transaction detail flattened into columns.
- A dispatcher picks a job for that row based on the event type and your account settings, and puts it on a queue.
- The job writes to QuickBooks through a service layer, then re-reads what it wrote to check it.
Only step 4 talks to QuickBooks. Steps 1 through 3 are all buffering, and each of them can hold a row for reasons that have nothing to do with anything being broken.
The important structural point is that none of these steps is synchronous with your Stripe dashboard. Stripe showing a successful charge means Stripe finished. It says nothing about where that charge is in the four steps above, and there is no reason to expect the two views to agree at any given second.
Stripe's side: delivery is not instant and not ordered
Before Acodei sees anything, Stripe has to deliver the event, and Stripe is explicit about what it does and does not promise.
On retries, Stripe's webhook documentation states that Stripe "attempts to deliver events to your destination for up to three days with an exponential back off in live mode." Three days is not a typo. If a delivery attempt fails, the next attempt is not immediate, and the gap grows.
On ordering, Stripe is blunter: "Stripe doesn't guarantee the delivery of events in the order that they're generated." The documentation gives the example of creating a subscription, which can produce customer.subscription.created, invoice.created, invoice.paid and charge.created in an order that does not match the order you would draw them in.
And on duplicates: "Webhook endpoints might occasionally receive the same event more than once."
Three consequences follow, and they explain a lot of apparently strange sequencing:
- A charge and its invoice can arrive out of order, so a record can appear to be waiting on a sibling that Stripe has not delivered yet.
- Retried events arrive late by design, not by fault. An event that failed its first delivery attempt may land minutes later.
- Any receiving system has to assume it will see the same event twice, which is where half of the flag machinery in the next section comes from.
Stripe also names the canonical burst in its own best-practice guidance: it recommends processing events with an asynchronous queue because "any large spike in webhook deliveries (for example, during the beginning of the month when all subscriptions renew) might overwhelm your endpoint hosts."
If your books look slowest on the first of the month, that is not a coincidence and it is not a coincidence unique to Acodei. It is the shape of subscription billing.
QuickBooks' side: the write is the scarce resource
Every QuickBooks write Acodei performs goes through a single service wrapper, and that wrapper does three things before the call reaches Intuit.
It applies a shared rate limit per QuickBooks company. The limit is shared across workers rather than applied per worker, so putting more workers on your backlog does not increase the rate at which your company gets written to. This is deliberate. QuickBooks Online enforces its own API call limits and throttling, published in Intuit's developer documentation, and the practical difference between staying under a limit and bouncing off it is enormous. Requests that get throttled do not simply cost you the request. They cost you the retry and the queue depth behind it.
Because the limit is scoped per company, separate QuickBooks companies do not compete with each other. Within one company, though, everything draws on the same budget: charges, refunds, invoices, payouts, and the daily summaries alike.
It proactively refreshes an expiring token. Rather than making the call, receiving an auth failure, and reacting, the wrapper checks first and refreshes ahead of the call. That is a round trip to Intuit happening before the round trip you were actually waiting on.
It retries once after a 401, by refreshing. Exactly one retry, exactly for that case. This is the part most people over-read. It is not a general retry-with-backoff safety net that will eventually push any failed write through. A write that fails for a real reason, a missing product mapping or a deleted account, fails, and it fails visibly.
That distinction is the whole point. Auth wobble is handled silently. Everything else surfaces.
What a failure actually looks like, so you can rule it out
Knowing what a failure looks like is how you decide whether waiting is the right call, and the signature is specific.
When a QuickBooks write fails, the Transaction row records an error_msg, moves to a failed status, clears its queue and executing flags, and pushes an update to the dashboard over a live connection. Some failure classes also send an email.
Read that list again, because every item is an observable:
- There is a message. A failed row is never silent about why.
- The status changes. It does not stay on "in progress".
- The flags are cleared, which is what makes the row eligible to be retried at all.
- The dashboard updates without you refreshing it.
So the diagnostic is simple. A row with no error message and no status change has not failed. It is somewhere in the queue. The absence of an error is information, not an absence of information.
If instead there is no row in the Data Feed at all, that is a genuinely different problem with different causes, and Stripe webhook events that never reach QuickBooks covers it. Everything here assumes the row exists.
The two flags that make a healthy row look frozen
Two internal flags on every Transaction row explain most of the "it is just sitting there" experience, and both exist to prevent duplicate records rather than to sequence work.
The queue flag is set once a row has been dispatched to a job. A row already carrying it is never dispatched again. A second value is reserved for rows held for batch processing.
The executing flag is set while a job is actually mid-run. If a second worker picks up the same row, it sees the flag and returns early instead of writing a second copy.
Both are anti-duplication mechanisms, and both have the same side effect: a row that is genuinely in flight looks, from the outside, exactly like a row that is doing nothing. There is no "currently writing" indicator to watch. There is a flag you cannot see, and it is doing its job.
These flags are also why patience beats intervention. A row that has already been dispatched carries the queue flag and is not dispatched a second time, so there is no way to nudge it into a faster lane. A resync is not a nudge either: it is a real pair of operations, described further down, and both of them draw on the same per-company rate limit budget the row is already waiting on.
The status column names several of these waits explicitly, including a payout that is waiting on other transactions to sync first, and a half-synced state that parks a row so a retry cannot duplicate it. Each value is a different decision, and what each Data Feed status is telling you walks through the full set rather than repeating it here.
Batching: when slower is the setting you chose
Some of the delay is not throttling at all. It is a configuration deciding to write fewer, larger records.
Daily summary mode. Acodei supports two sync modes. Real-time posts each charge and refund individually. Daily summary aggregates a day into one set of records. On a daily summary account, an individual charge is not late when it has not appeared an hour later. It is not going to appear individually at all. The tradeoff between the two, and what each does to your reconciliation, is covered in daily summary versus real-time sync.
Batch dispatch. Some accounts do not dispatch row by row. Accounts using the customer balance tracker chain their invoice-related jobs together in invoice.created-first order, so an invoice payment genuinely waits for its invoice rather than racing it. Accounts that batch expenses daily collapse to one journal-entry job per account per day.
In both cases the row is not stuck behind a limit. It is waiting for a sibling or for a scheduled collapse, and it will move when that happens.
This matters for interpretation. "Nothing has appeared in twenty minutes" means something different on a real-time account than on an account that batches, and the same observation should lead you to a different conclusion. Check which mode you are on before you diagnose latency.
The validation step you are also waiting for
The write is not the last thing that happens. Jobs validate after writing, and that validation is a second round trip.
For invoices, the QuickBooks invoice is re-read and its total compared against the Stripe amount. On a mismatch the QuickBooks invoice is deleted and the row is failed with an amount-mismatch message. Note the scope precisely: this exact-match check runs only when QuickBooks tax is enabled for the account. It is not an unconditional check on every invoice, and assuming otherwise leads people to expect a guard that is not running for them.
Daily balance summary totals get their own dedicated validation service.
So on a tax-enabled account, an invoice that has not appeared may have been written, re-read, found wrong, and deleted, all inside the window you spent watching for it. That path ends in a failed row with a message, which brings you back to the diagnostic above: look for the message.
Volume: the one documented ceiling
There is exactly one published volume figure worth knowing, and it is narrow.
Very high invoice volumes, above roughly 5,000 per day, may hit QuickBooks rate limits. Batching for that case is planned rather than shipped. As of the current documentation, no account has been observed running at that volume.
Two honest readings of that:
- If you are nowhere near 5,000 invoices a day, and almost everyone is, invoice volume is not your explanation. Look at mode and batching first.
- If you are approaching it, the constraint is real and worth a conversation before you get there rather than after.
Historical imports are the other place volume shows up, because a backfill is by definition a large amount of writing compressed into a short window against the same per-company limit. That is why imports run in batches, and the historical import playbook covers what to expect from one.
What to do instead of resyncing
Resync exists and it works. It is just the wrong first move for a slow row, because of what it actually does: it books the QuickBooks-side delete first, then re-books the transaction for its job. That is a delete plus a rewrite, two operations against the same rate limit, to replace a record that in many cases was going to arrive on its own.
A better order:
- Check for an error message. No message and no status change means it has not failed. This rules out most of the panic in one step.
- Check your sync mode. On daily summary, individual charges are never going to appear individually.
- Check the date. Start of month, a launch, or a migration all mean queue depth, and queue depth means waiting is the expected experience.
- Check whether you are inside a historical import. A backfill and your live traffic draw on the same budget.
- Then wait. Genuinely. This is the step people skip, and it is the one that resolves the largest share of cases.
- If it failed, resync from the Data Feed, rather than fixing it by hand in QuickBooks. That is the documented best practice, and it keeps the correction on the path the sync can see. For invoices specifically, resyncing a Stripe invoice has its own sequence worth understanding first.
The one thing not to do is resync in bulk out of impatience. Every resync is two operations, they queue behind everything already waiting, and the flags mean a row already in flight will not go faster for being asked twice.
The short version
Delay and failure look identical for the first thirty seconds and completely different after that. A failed row carries a message, changes status, and tells your dashboard. A slow row carries nothing, because nothing has gone wrong: it is behind a per-company rate limit, or a token refresh, or a batch that has not run yet, or a sibling transaction that Stripe has not delivered in the order you assumed.
The layer between Stripe and QuickBooks is built to protect your books from duplicates and from throttling, and the price of that protection is that patience is frequently the correct diagnosis. The error message is the signal. Its absence is also a signal, and it says wait.
If you want the whole path set up correctly from the start, how to sync Stripe to QuickBooks walks through the setup, and Start a free trial when you are ready to stop reconciling Stripe by hand.
Frequently asked questions
How long should a Stripe to QuickBooks sync take?
There is no published figure, and anyone quoting one precisely is guessing. What is documented is the shape: writes go through a shared per-QuickBooks-company rate limit, so time to appear depends on how much else is queued for that same company. On a quiet day with real-time sync, records land promptly. On the first of the month, when Stripe itself warns that subscription renewals produce a large spike in webhook deliveries, the same account will be slower.
Is my sync broken if a row has been "in progress" for an hour?
Not necessarily, and the test is specific. A failed row records an error message, moves to a failed status, clears its flags, and pushes an update to your dashboard. A row with no message and no status change has not failed. It is queued.
Does resyncing make a slow row go faster?
No, and it usually makes things marginally slower. A resync books the QuickBooks-side delete first and then re-books the transaction for its job, so it is two operations against the same rate limit that the row is already waiting on. A row that has already been dispatched also carries a queue flag that prevents it being dispatched again.
Why did my QuickBooks record appear before a related one?
Stripe does not guarantee that events arrive in the order they were generated, and states so directly in its webhook documentation. A subscription can generate its subscription, invoice, payment and charge events in an order that does not match the order you would expect. Some Acodei accounts also chain invoice-related jobs deliberately, in invoice.created-first order, so a payment waits for its invoice rather than racing it.
Does Acodei retry a failed QuickBooks write?
Only in one specific case. The service wrapper retries once after an authentication failure by refreshing the token, and it also refreshes tokens proactively before they expire. Failures for other reasons, such as a missing product mapping, are not retried in a loop. They surface as a failed row with an error message so you can fix the cause.
What happens at very high invoice volume?
Documented limitation: invoice volumes above roughly 5,000 per day may hit QuickBooks rate limits, and batching for that case is planned rather than shipped. No account has been observed at that volume to date. Below that, invoice volume is unlikely to be the reason a record is slow.
Automate your Stripe to QuickBooks sync
Save hours every month. Acodei automatically syncs your Stripe transactions, invoices, and payouts to QuickBooks Online.
How Acodei handles this in your stack
Stripe QuickBooks Integration
See how Acodei syncs Stripe payments, fees, refunds, invoices, and payouts into QuickBooks Online automatically.
Or go straight to a capability
Advanced Product Mapping
Map Stripe products to QuickBooks with rule-based logic on product ID, price ID, metadata, and account. Set rule priority and extend mapping to refunds and fees.
Automated Invoice Sync
Bring Stripe invoices into QuickBooks and auto-apply payments and credit memos, with numbering, invoice matching, and quantity tracking to cut double-entry.
Multi-Currency Mastery
Sync Stripe transactions across currencies with automatic exchange rate handling, currency-specific customer records, and invoice-level multicurrency.
Class Mapping
Map Stripe products to QuickBooks classes for scalable categorization and multi-entity reporting, enabling precise insights without manual effort.
Historical Data Import
Backfill historical Stripe data into QuickBooks by month range. Preview volume and cost before syncing so reporting starts from a complete baseline.
How to Connect Stripe to QuickBooks Online
Connect Stripe to QuickBooks Online in minutes. Acodei links both accounts with secure OAuth and syncs payments, fees, refunds, and payouts automatically.
Reconcile Stripe Payments in QuickBooks
Reconcile Stripe in QuickBooks Online automatically. Acodei splits out fees, matches payouts to deposits, and keeps every charge audit-ready.
Related articles
Acodei Journal
A Stripe Invoice Resync Rebuilds Two QuickBooks Records
Acodei Content Team
A Stripe Invoice Resync Rebuilds Two QuickBooks Records
8/13/2026
Acodei Journal
What Each Acodei Data Feed Status Actually Means
Acodei Content Team
What Each Acodei Data Feed Status Actually Means
8/13/2026
Acodei Journal
What Duplicate Protection Actually Checks in QuickBooks
Acodei Content Team
What Duplicate Protection Actually Checks in QuickBooks
8/10/2026
Get more operational finance guides like this one
We will only send high-value product and finance content.