Skip to content

fix: batch list() calls in all selector-based update methods to prevent unbounded memory consumption#15011

Open
ashif323 wants to merge 3 commits intomedusajs:developfrom
ashif323:fix/unbounded-list-queries-update-methods
Open

fix: batch list() calls in all selector-based update methods to prevent unbounded memory consumption#15011
ashif323 wants to merge 3 commits intomedusajs:developfrom
ashif323:fix/unbounded-list-queries-update-methods

Conversation

@ashif323
Copy link
Copy Markdown

@ashif323 ashif323 commented Apr 5, 2026

What

Replaces all unbounded list(selector, {}) calls with paginated batching
(BATCH_SIZE=500) across 9 selector-based update methods in the product and
cart modules.

Why

When any of these methods are called with a broad selector object (e.g.
{ status: "active" }, { collection_id: "x" }), the internal list()
call had an empty config — no take, no skip. On large datasets this
loads all matching records into Node.js heap simultaneously, risking OOM
crashes and severe performance degradation.

This is the runtime consequence of the TSDoc inaccuracy noted in PR #14920
— the docs said limit was 15, reality is no limit at all.

Affected methods

packages/modules/product/src/services/product-module-service.ts

  • updateProducts()
  • updateProductVariants()
  • updateProductTags()
  • updateProductTypes()
  • updateProductOptions()
  • updateProductCollections()
  • updateProductCategories_()
  • updateProductOptionValues()

packages/modules/cart/src/services/cart-module.ts

  • updateCarts()

How

// Before — unbounded
const products = await this.productService_.list(idOrSelector, {}, sharedContext)

// After — batched in chunks of 500
do {
  batch = await this.productService_.list(
    idOrSelector,
    { take: BATCH_SIZE, skip },
    sharedContext
  )
  normalizedInput.push(...batch.map(p => ({ id: p.id, ...data })))
  skip += BATCH_SIZE
} while (batch.length === BATCH_SIZE)

The safe pattern already exists in the codebase at updateProducts_()
with take: productIds.length — this PR extends it consistently to all
selector-based paths.

Notes

Closes #15010


Note

Medium Risk
Touches multiple selector-based update code paths in cart/product modules; while behavior should be equivalent, pagination loops could change update coverage under concurrent writes or if service pagination semantics differ.

Overview
Prevents potential OOMs when selector-based update methods are called with broad filters by replacing unbounded list(selector, {}) calls with paginated batching (take/skip, BATCH_SIZE = 500).

This batching is applied to updateCarts() and the product module’s selector-based update methods (products, variants, tags, types, options, collections, categories, option values), accumulating IDs batch-by-batch before issuing the final update()/update*_() call.

Reviewed by Cursor Bugbot for commit 3e2ced9. Bugbot is set up for automated code reviews on this repo. Configure here.

…nt unbounded memory consumption

Affected methods in product-module-service.ts:
- updateProducts()
- updateProductVariants()
- updateProductTags()
- updateProductTypes()
- updateProductOptions()
- updateProductCollections()
- updateProductCategories_()
- updateProductOptionValues()

Affected methods in cart-module.ts:
- updateCarts()

All selector-based branches previously called list(selector, {}) with an
empty config — no take, no skip. On large datasets this loads all matching
records into Node.js heap memory simultaneously, risking OOM crashes.

Each affected method now batches in chunks of 500 using take/skip pagination,
following the safe pattern already used in updateProducts_() at L1748.

Fixes medusajs#15010
@ashif323 ashif323 requested a review from a team as a code owner April 5, 2026 11:23
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 5, 2026

🦋 Changeset detected

Latest commit: 68b439e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 77 packages
Name Type
@medusajs/product Patch
@medusajs/cart Patch
@medusajs/medusa Patch
integration-tests-http Patch
@medusajs/test-utils Patch
@medusajs/medusa-oas-cli Patch
@medusajs/analytics Patch
@medusajs/api-key Patch
@medusajs/auth Patch
@medusajs/caching Patch
@medusajs/currency Patch
@medusajs/customer Patch
@medusajs/file Patch
@medusajs/fulfillment Patch
@medusajs/index Patch
@medusajs/inventory Patch
@medusajs/link-modules Patch
@medusajs/locking Patch
@medusajs/notification Patch
@medusajs/order Patch
@medusajs/payment Patch
@medusajs/pricing Patch
@medusajs/promotion Patch
@medusajs/rbac Patch
@medusajs/region Patch
@medusajs/sales-channel Patch
@medusajs/settings Patch
@medusajs/stock-location Patch
@medusajs/store Patch
@medusajs/tax Patch
@medusajs/translation Patch
@medusajs/user Patch
@medusajs/workflow-engine-inmemory Patch
@medusajs/workflow-engine-redis Patch
@medusajs/draft-order Patch
@medusajs/oas-github-ci Patch
@medusajs/cache-inmemory Patch
@medusajs/cache-redis Patch
@medusajs/event-bus-local Patch
@medusajs/event-bus-redis Patch
@medusajs/analytics-local Patch
@medusajs/analytics-posthog Patch
@medusajs/auth-emailpass Patch
@medusajs/auth-github Patch
@medusajs/auth-google Patch
@medusajs/caching-redis Patch
@medusajs/file-local Patch
@medusajs/file-s3 Patch
@medusajs/fulfillment-manual Patch
@medusajs/locking-postgres Patch
@medusajs/locking-redis Patch
@medusajs/notification-local Patch
@medusajs/notification-sendgrid Patch
@medusajs/payment-stripe Patch
@medusajs/core-flows Patch
@medusajs/framework Patch
@medusajs/js-sdk Patch
@medusajs/modules-sdk Patch
@medusajs/orchestration Patch
@medusajs/types Patch
@medusajs/utils Patch
@medusajs/workflows-sdk Patch
@medusajs/http-types-generator Patch
@medusajs/cli Patch
@medusajs/deps Patch
@medusajs/telemetry Patch
@medusajs/admin-bundler Patch
@medusajs/admin-sdk Patch
@medusajs/admin-shared Patch
@medusajs/admin-vite-plugin Patch
@medusajs/dashboard Patch
@medusajs/icons Patch
@medusajs/toolbox Patch
@medusajs/ui-preset Patch
create-medusa-app Patch
medusa-dev-cli Patch
@medusajs/ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 5, 2026

@ashif323 is attempting to deploy a commit to the medusajs Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3e2ced9. Configure here.

)
skip += BATCH_SIZE
} while (batch.length === BATCH_SIZE)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Batching code indented at wrong level across all methods

Medium Severity

All 9 batching blocks are indented at 2 spaces with the closing } at column 0, while the surrounding else block and method body use 4–6 space indentation. This makes the batching code visually appear to live outside the else block (or even outside the method), even though brace-matching is syntactically correct. A future developer modifying this code could easily misread the block structure and introduce a scoping bug.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3e2ced9. Configure here.

normalizedInput.push(...batch.map((variant) => ({ id: variant.id, ...data })))
skip += BATCH_SIZE
} while (batch.length === BATCH_SIZE)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Identical batching pattern duplicated nine times across methods

Low Severity

The same do...while batching loop with BATCH_SIZE, skip, and batch variables is copy-pasted across 9 methods. A shared helper (e.g., collectBatchedIds(service, selector, context)) would consolidate this into one implementation, reducing maintenance burden and the risk of inconsistent fixes if the pattern ever needs to change.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3e2ced9. Configure here.

Addresses Cursor Bugbot feedback on PR medusajs#15011:
- Extracts repeated do/while batching pattern into a shared private helper
- Fixes indentation inconsistency across all affected methods
- No behavior change

The productCategoryService_ required a cast to IMedusaInternalService<any>
as it uses a different concrete type (ProductCategoryService) than the
other services in this module.
@cursor
Copy link
Copy Markdown

cursor bot commented Apr 5, 2026

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 5, 2026

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@medusa-os-bot
Copy link
Copy Markdown

medusa-os-bot bot commented Apr 9, 2026

Thank you for your contribution!

After reviewing this PR, we need a few things addressed before we can move forward:

Required changes:

  • Please fill in the Testing section of the PR description — explain how this fix can be verified (e.g., calling an update method with a broad selector on a large dataset, or a unit/integration test setup). This section is required by our PR template.
  • The changeset file appears to bump 77 packages, but only @medusajs/cart and @medusajs/product were changed. Please regenerate the changeset with yarn changeset selecting only those two packages.
  • The changeset message must follow the format fix(cart, product): short description. Please update it accordingly.
  • Tests are missing. This change modifies critical update paths in the product and cart modules — please add integration or unit tests that verify the batched pagination behavior (e.g., confirm all records are updated when a selector matches more than BATCH_SIZE items).

Concerns:

The normalizedInput array is still accumulated across all batches before passing it to the underlying update*() call. This means memory usage is still proportional to the total number of matched records, not to BATCH_SIZE. For a complete fix, the downstream update call should also be batched. This is worth noting — a follow-up issue or a note in the PR would help clarify the intended scope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Unbounded list() queries in updateProducts() and updateCarts() may cause memory exhaustion

1 participant