You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Due for payment 2026-04-30] Duplicate expense - Page become unresponsive when duplicating bulk expenses #87604
Version Number: v9.3.58-0 Reproducible in staging?: Yes Reproducible in production?: N/A - new feature, doesn't exist in prod If this was caught during regression testing, add the test name, ID and link from BrowserStack:#84657 Email or phone of affected tester (no customers):testpayment935@gmail.com Issue reported by: Applause Internal Team Bug source: Exploratory - Significant User Experience Deterioration Device used: Windows 11 pro /Chrome App Component: Money Requests
Action Performed:
Navigate to staging.new.expensify.com
Sign in with 200+ expense account ( you can duplicate until you have that amount)
Go to reports and select all expense and duplicate
Expected Result:
The expense will be duplicated without an issue
Actual Result:
The page become unresponsive when duplicating a large amount of expense
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
Identify the pull request that introduced this issue and revert it.
Classification: Frontend bug Causing PR: #84657 - "[Duplicate Expense] Allow bulk duplication." by @Krishna2323 (High confidence) Related Issues: #84281
Recommendation: REVERT
PR #84657 introduced a synchronous loop that calls duplicateExpenseTransaction once per selected transaction with no batching or throttling. With 200+ expenses selected, this triggers hundreds of back-to-back Onyx writes and API preparations on the main JS thread, blocking the event loop and making the page unresponsive.
Assigned: @Krishna2323 (PR author), @JS00001@situchan (approving reviewers) Labels: No label changes needed — DeployBlockerCash is correct (frontend App PR caused the issue; this is staging-only and does not exist in production)
The bug is reproducible on staging and not applicable to production since this is a new feature
The issue was reported against version v9.3.58-0, which matches the current staging deploy
Verification
Affected code: src/libs/actions/IOU/Duplicate.ts — specifically the bulkDuplicateExpenses function
Confirmation: PR [Duplicate Expense] Allow bulk duplication. #84657 adds src/libs/actions/IOU/Duplicate.ts, src/hooks/useBulkDuplicateAction.ts, and src/components/Search/BulkDuplicateHandler.tsx — all directly on the path of the reported bug
Root Cause
bulkDuplicateExpenses in Duplicate.ts (lines 960-1034) runs a synchronous for loop over all selected transactions, calling duplicateExpenseTransaction for each one. Each call performs complex work including building optimistic Onyx updates, generating new report/transaction IDs, and preparing API parameters. With 200+ transactions this executes entirely on the main JS thread without yielding, causing a browser hang. There is no batching, chunking, or async deferral between iterations.
bulkDuplicateExpenses() in src/libs/actions/IOU/Duplicate.ts:995-1031 runs a synchronous for-loop over all selected transactions. Each iteration calls duplicateExpenseTransaction() → requestMoney() → API.write(), which synchronously applies ~10-15 Onyx optimistic updates via Onyx.update(). For 200+ expenses, this means 2000-3000+ synchronous Onyx state updates in a single uninterrupted JS frame, completely blocking the UI thread.
There is no batching, chunking, setTimeout yielding, or InteractionManager usage anywhere in this path.
What changes do you think we should make in order to solve the problem?
Break the synchronous loop in bulkDuplicateExpenses() (Duplicate.ts:995) into async chunks that yield to the event loop between batches. For example, process 5-10 transactions per chunk using setTimeout(..., 0) or requestAnimationFrame between chunks so the browser can paint and handle user input between batches.
A simple approach:
Convert the for-loop into an async function that processes a small batch (e.g., 5-10 items), then yields via await new Promise(resolve => setTimeout(resolve, 0)) before the next batch.
Optionally show a progress indicator during the operation so the user knows duplication is in progress.
What alternative solutions did you explore? (Optional)
Single batched API call: Instead of calling API.write() per transaction, send all duplications in one API command. This would be more efficient but requires backend changes and a new API endpoint — higher effort for a deploy blocker fix.
Web Worker: Offload Onyx updates to a worker thread. Not feasible since Onyx updates must happen on the main thread to trigger React re-renders.
Investigation details
Data flow causing the freeze:
User clicks "Duplicate" with 200+ selected transactions
Steps 4-6 repeat 200+ times without ever yielding to the event loop
JS thread is blocked for the entire duration → page unresponsive
Next Steps for Contributor+ team: Reply with @MelvinBot implement this to create a draft PR, @MelvinBot <your feedback> to refine this analysis, or explain why you are rejecting Melvin's proposal.
changed the title [-]Duplicate expense - Page become unresponsive when duplicating bulk expenses[/-][+][Due for payment 2026-04-30] Duplicate expense - Page become unresponsive when duplicating bulk expenses[/+]on Apr 23, 2026
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.3.61-4 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
@Krishna2323 / @situchan The PR fixing this issue has been merged! The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the BugZero Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]
Activity
melvin-bot commentedon Apr 10, 2026
You have been assigned to this deploy blocker because you recently merged this PR: #84657
melvin-bot commentedon Apr 10, 2026
💬 A slack conversation has been started in #expensify-open-source
github-actions commentedon Apr 10, 2026
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
OSBotify commentedon Apr 10, 2026
Investigation Summary
Classification: Frontend bug
Causing PR: #84657 - "[Duplicate Expense] Allow bulk duplication." by @Krishna2323 (High confidence)
Related Issues: #84281
Recommendation: REVERT
PR #84657 introduced a synchronous loop that calls
duplicateExpenseTransactiononce per selected transaction with no batching or throttling. With 200+ expenses selected, this triggers hundreds of back-to-back Onyx writes and API preparations on the main JS thread, blocking the event loop and making the page unresponsive.Assigned: @Krishna2323 (PR author), @JS00001 @situchan (approving reviewers)
Labels: No label changes needed —
DeployBlockerCashis correct (frontend App PR caused the issue; this is staging-only and does not exist in production)Detailed Analysis
Evidence
Verification
src/libs/actions/IOU/Duplicate.ts— specifically thebulkDuplicateExpensesfunctionsrc/libs/actions/IOU/Duplicate.ts,src/hooks/useBulkDuplicateAction.ts, andsrc/components/Search/BulkDuplicateHandler.tsx— all directly on the path of the reported bugRoot Cause
bulkDuplicateExpensesinDuplicate.ts(lines 960-1034) runs a synchronousforloop over all selected transactions, callingduplicateExpenseTransactionfor each one. Each call performs complex work including building optimistic Onyx updates, generating new report/transaction IDs, and preparing API parameters. With 200+ transactions this executes entirely on the main JS thread without yielding, causing a browser hang. There is no batching, chunking, or async deferral between iterations.MelvinBot commentedon Apr 10, 2026
Proposal
What is the root cause of that problem?
bulkDuplicateExpenses()insrc/libs/actions/IOU/Duplicate.ts:995-1031runs a synchronous for-loop over all selected transactions. Each iteration callsduplicateExpenseTransaction()→requestMoney()→API.write(), which synchronously applies ~10-15 Onyx optimistic updates viaOnyx.update(). For 200+ expenses, this means 2000-3000+ synchronous Onyx state updates in a single uninterrupted JS frame, completely blocking the UI thread.There is no batching, chunking,
setTimeoutyielding, orInteractionManagerusage anywhere in this path.What changes do you think we should make in order to solve the problem?
Break the synchronous loop in
bulkDuplicateExpenses()(Duplicate.ts:995) into async chunks that yield to the event loop between batches. For example, process 5-10 transactions per chunk usingsetTimeout(..., 0)orrequestAnimationFramebetween chunks so the browser can paint and handle user input between batches.A simple approach:
await new Promise(resolve => setTimeout(resolve, 0))before the next batch.What alternative solutions did you explore? (Optional)
API.write()per transaction, send all duplications in one API command. This would be more efficient but requires backend changes and a new API endpoint — higher effort for a deploy blocker fix.Investigation details
Data flow causing the freeze:
useBulkDuplicateAction.handleDuplicate()callsbulkDuplicateExpenses()bulkDuplicateExpenses()enters a synchronous for-loop atDuplicate.ts:995duplicateExpenseTransaction()→createExpenseByType()→requestMoney()requestMoney()callsAPI.write()→prepareRequest()→Onyx.update(optimisticData)synchronouslybuildOnyxDataForMoneyRequest(src/libs/actions/IOU/index.ts:1668-2386) generates ~36 Onyx push operations per transaction (~10-15 optimistic ones applied synchronously)Next Steps for Contributor+ team: Reply with
@MelvinBot implement thisto create a draft PR,@MelvinBot <your feedback>to refine this analysis, or explain why you are rejecting Melvin's proposal.situchan commentedon Apr 10, 2026
200+ expense 😮
This feature is gated behind production so we can demote.
JS00001 commentedon Apr 10, 2026
Demoti9ng since this is locked from prod
15 remaining items
jamesdeanexpensify commentedon Apr 13, 2026
Love that copy - maybe just update the header to
Duplicate expensesto match the button?trjExpensify commentedon Apr 13, 2026
Which one to confirm? My written edit or the one in the screenshot?
jamesdeanexpensify commentedon Apr 13, 2026
Ah, I actually don't mind the one in the screenshot.
JS00001 commentedon Apr 14, 2026
@Krishna2323 lets use the current one then
JS00001 commentedon Apr 17, 2026
PR to fix this blocker is merged: #87697
melvin-bot commentedon Apr 21, 2026
If you are the assigned CME please investigate whether the linked PR caused a regression and leave a comment with the results.
If a regression has occurred and you are the assigned CM follow the instructions here.
If this regression could have been avoided please consider also proposing a recommendation to the PR checklist so that we can avoid it in the future.
[-]Duplicate expense - Page become unresponsive when duplicating bulk expenses[/-][+][Due for payment 2026-04-30] Duplicate expense - Page become unresponsive when duplicating bulk expenses[/+]melvin-bot commentedon Apr 23, 2026
Reviewinglabel has been removed, please complete the "BugZero Checklist".melvin-bot commentedon Apr 23, 2026
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.3.61-4 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-04-30. 🎊
For reference, here are some details about the assignees on this issue:
melvin-bot commentedon Apr 23, 2026
@Krishna2323 / @situchan The PR fixing this issue has been merged! The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the BugZero Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]