DEV Community

Magevanta
Magevanta

Posted on • Originally published at magevanta.com

Magento 2 B2B Performance Optimization: Shared Catalogs, Quotes & Company Accounts

Adobe Commerce B2B unlocks powerful functionality for wholesale and enterprise merchants — company accounts, shared catalogs, negotiable quotes, quick order, and requisition lists. But these features come with a significant performance cost if left unconfigured. In this post we dig into the most common B2B bottlenecks and how to systematically address each one.

Why B2B Is Different from B2C

Standard Magento 2 performance advice — Redis caching, Varnish, OPcache, flat tables — still applies to B2B. But B2B merchants face a set of additional challenges:

  • Shared catalogs create per-company product visibility rules that bypass the standard catalog cache.
  • Negotiable quotes generate large quote objects with complex price recalculations on every update.
  • Company hierarchies add permission checks to nearly every storefront request for logged-in users.
  • Requisition lists issue repeated save/load queries against the negotiable_quote and requisition_list tables.

The result: B2B storefronts often run 2–4× slower than equivalent B2C stores with no additional configuration.

1. Shared Catalog — The Hidden Query Bomb

Shared catalogs work by filtering product visibility per company via the shared_catalog_product_item table. Each catalog page load for a logged-in B2B customer hits this table with large JOIN queries.

What to profile

Run EXPLAIN on a category page query with shared catalogs enabled:

EXPLAIN SELECT e.entity_id, e.sku, e.name
FROM catalog_product_entity e
INNER JOIN shared_catalog_product_item sci
  ON sci.sku = e.sku AND sci.customer_group_id = 5
WHERE e.status = 1;
Enter fullscreen mode Exit fullscreen mode

Missing indexes on (sku, customer_group_id) are the most common culprit.

Fixes

Add a composite index if it's missing (check your version — Adobe Commerce 2.4.5+ has this):

ALTER TABLE shared_catalog_product_item
  ADD INDEX IDX_SKU_GROUP (sku, customer_group_id);
Enter fullscreen mode Exit fullscreen mode

Enable flat catalog for B2B stores with large catalogs (>50k SKUs). Despite being deprecated, flat tables still dramatically reduce EAV JOINs on category pages:

bin/magento config:set catalog/frontend/flat_catalog_product 1
bin/magento config:set catalog/frontend/flat_catalog_category 1
bin/magento indexer:reindex catalog_product_flat catalog_category_flat
Enter fullscreen mode Exit fullscreen mode

Limit shared catalog depth. Every additional custom catalog multiplies reindex time and query complexity. Audit whether all catalogs are actually in use:

SELECT id, name, customer_group_id, created_at
FROM shared_catalog
ORDER BY created_at DESC;
Enter fullscreen mode Exit fullscreen mode

Delete or merge unused catalogs via the Admin panel before they compound the problem.

2. Negotiable Quotes — Price Recalculation Is Expensive

Negotiable quotes are notorious for triggering full cart recalculations. Every time a sales rep adjusts a line item, Magento recalculates the entire quote: shipping estimates, tax, discounts, custom prices. On quotes with 200+ line items this can take 10–30 seconds.

Reduce recalculation triggers

In app/etc/config.php or via Admin, limit quote recalculation to explicit submit actions rather than on every save:

Stores → Configuration → B2B Features → Default B2B Payment Methods
Enter fullscreen mode Exit fullscreen mode

More importantly, disable automatic quote totals recalculation on every admin edit. Patch the Magento\NegotiableQuote\Model\Quote\Totals class via a plugin to defer recalculation:

// Plugin: defer recalculation to explicit recalculate() call only
public function aroundRecalculateQuote(
    \Magento\NegotiableQuote\Model\Quote\Totals $subject,
    callable $proceed,
    bool $force = false
): void {
    if ($force) {
        $proceed($force);
    }
    // Skip automatic recalculation triggered by save events
}
Enter fullscreen mode Exit fullscreen mode

Apply carefully and test pricing accuracy thoroughly.

Index and archive old quotes

The quote and negotiable_quote tables grow unbounded on active B2B stores. Queries slow down as these tables exceed millions of rows.

-- Find quotes older than 6 months that are closed/declined
SELECT COUNT(*) FROM negotiable_quote nq
INNER JOIN quote q ON q.entity_id = nq.quote_id
WHERE nq.status IN ('closed', 'declined')
  AND q.updated_at < DATE_SUB(NOW(), INTERVAL 6 MONTH);
Enter fullscreen mode Exit fullscreen mode

Archive or purge stale quotes on a regular schedule using a custom cron job. Adobe Commerce does not do this automatically.

3. Company Account Permission Checks

Every request from a logged-in B2B user triggers a company permission check via Magento\Company\Model\Authorization. On storefronts with complex role hierarchies (company → division → user), this becomes expensive.

Cache company permissions

Company permission data changes rarely. Add a Redis-backed cache layer:

// In your around plugin on CompanyManagement::getByCustomerId()
$cacheKey = 'company_user_' . $customerId;
$cached = $this->cache->load($cacheKey);
if ($cached) {
    return $this->serializer->unserialize($cached);
}
$result = $proceed($customerId);
$this->cache->save(
    $this->serializer->serialize($result),
    $cacheKey,
    ['company_permissions'],
    3600
);
return $result;
Enter fullscreen mode Exit fullscreen mode

Invalidate the tag company_permissions on company/role changes via an observer on company_save_after.

Minimize role tree depth

Deeply nested company structures (5+ levels) multiply permission checks. Encourage merchants to keep hierarchies flat: company → department → user (3 levels maximum). Beyond that, query count grows exponentially.

4. Requisition Lists — Silent Scalability Problem

Requisition lists seem lightweight but they issue a database read on every storefront visit for logged-in B2B users. Customers with 20+ requisition lists of 100+ items each generate dozens of queries per page.

Lazy-load requisition list counts

The storefront header displays a requisition list count. By default this is loaded synchronously. Move it to a separate AJAX call that runs after page load:

<!-- layout/default.xml -->
<block class="Magento\RequisitionList\Block\RequisitionList\Counter"
       name="requisition.list.counter"
       template="Magento_RequisitionList::counter.phtml">
    <arguments>
        <argument name="lazy_load" xsi:type="boolean">true</argument>
    </arguments>
</block>
Enter fullscreen mode Exit fullscreen mode

Implement the lazy-load controller endpoint at requisition_list/ajax/count and update the template to defer via fetch().

Paginate large requisition lists

A single requisition list with 500 items forces Magento to load all items before rendering. Add server-side pagination in your custom module:

$collection = $this->requisitionListItemRepository
    ->getList($searchCriteria->setPageSize(50)->setCurrentPage($page));
Enter fullscreen mode Exit fullscreen mode

This alone can reduce TTFB on the requisition list page from 4s to under 800ms for large lists.

5. B2B Indexer Performance

B2B adds several indexers that run on top of the standard Magento indexers:

  • shared_catalog_product_price — runs on every shared catalog change
  • b2b_company_structure — rebuilds company trees
  • negotiable_quote_grid — refreshes the admin quote grid

These indexers are not always set to Update on Schedule out of the box.

Force schedule mode for all B2B indexers

bin/magento indexer:set-mode schedule shared_catalog_product_price
bin/magento indexer:set-mode schedule b2b_company_structure
bin/magento indexer:set-mode schedule negotiable_quote_grid
Enter fullscreen mode Exit fullscreen mode

Verify the result:

bin/magento indexer:status | grep -E "shared_catalog|b2b_company|negotiable"
Enter fullscreen mode Exit fullscreen mode

All B2B indexers should show Update by Schedule.

6. HTTP Cache Strategy for B2B

Full-page caching is effectively disabled for logged-in B2B customers — they see personalized catalogs, prices, and credit limits. This means your origin servers absorb every B2B request.

Session-less catalog pages

For large B2B stores, build a "guest view" of catalog pages that is FPC-cacheable, with personalized data (price, availability) loaded via AJAX after page paint. This is the same technique used in B2C "hole punching" but applied more aggressively.

HTTP/2 push for critical B2B assets

B2B pages load more JavaScript (quote widget, company menu, requisition controls). Configure Nginx or your CDN to HTTP/2 push core B2B JS bundles:

location = /b2b-account {
    http2_push /pub/static/frontend/Magento/luma/en_US/Magento_NegotiableQuote/js/quote.min.js;
    http2_push /pub/static/frontend/Magento/luma/en_US/Magento_Company/js/company.min.js;
}
Enter fullscreen mode Exit fullscreen mode

7. Monitoring B2B Performance

Standard APM tools miss B2B-specific slow paths. Add custom instrumentation:

// In a plugin on NegotiableQuoteManagement::send()
$startTime = microtime(true);
$result = $proceed(...$args);
$elapsed = microtime(true) - $startTime;
if ($elapsed > 2.0) {
    $this->logger->warning('Slow negotiable quote send', [
        'quote_id' => $quoteId,
        'elapsed_ms' => round($elapsed * 1000),
    ]);
}
return $result;
Enter fullscreen mode Exit fullscreen mode

Track and alert on:

  • Negotiable quote recalculation time > 3s
  • Shared catalog reindex duration > 5 min
  • Requisition list page TTFB > 1s

Summary: B2B Performance Checklist

Area Action Impact
Shared catalog Add composite index on (sku, customer_group_id) High
Shared catalog Archive unused catalogs Medium
Negotiable quotes Defer auto-recalculation High
Negotiable quotes Archive closed/declined quotes Medium
Company permissions Cache per-user permission data in Redis High
Company structure Keep hierarchy ≤ 3 levels deep Medium
Requisition lists Lazy-load header counter Medium
Requisition lists Paginate large lists High
B2B indexers Set all to Update by Schedule High
HTTP caching AJAX-load personalized data, cache catalog shell High

B2B performance is not a single-fix problem — it requires work across the stack. Start with indexer modes and the shared catalog index, as those yield the highest return with the lowest risk. Then work through quote archiving and company permission caching. With these changes in place, most B2B storefronts can achieve sub-second category pages even under authenticated load.

Top comments (0)