← Back to blog

Magento 2 Customer Data Sections: How Private Content Works

Rodrigo Biazzi

Magento 2 caches full pages for every visitor, yet the header still shows the right name, the minicart shows the right items and the messages show up after adding to cart. That works because personal data does not come from the cached HTML. It comes from customer data sections, loaded by JavaScript after the page renders.

Understanding this mechanism is the difference between a store where full page cache works and one where somebody added cacheable="false" to make a greeting work and quietly disabled caching for every page. This article explains how sections work in Magento 2.4.8 and how to add your own.

The problem sections solve

With full page cache, the first visitor to a URL generates the HTML and every following visitor gets the same HTML from Varnish or the built in cache. Anything specific to a customer, such as their name, cart, wishlist count or a personalised price message, cannot be in that HTML. If it is, the next visitor sees it too.

Magento's answer is to keep the HTML generic and fetch private content separately:

  1. The cached page renders with placeholders bound to JavaScript components.
  2. The Magento_Customer/js/customer-data module checks the browser's local storage for each section.
  3. If a section is missing, expired or invalidated, it requests it from customer/section/load, which is never cached.
  4. Components bound to that section update automatically, because sections are Knockout observables.

The data is stored in local storage under mage-cache-storage, so most page views make no extra request at all. This is also why clearing cookies sometimes does not clear what you see in the minicart, while clearing local storage does.

Built in sections

Core modules register sections such as cart, customer, messages, wishlist, compare-products, directory-data and last-ordered-items. You can see the full list for your store by opening the Application tab in the browser developer tools and inspecting mage-cache-storage.

When sections are reloaded

Sections are invalidated in a few ways:

  • sections.xml: each module maps controller action paths to the sections they change. When the browser sends a POST to one of those actions, through AJAX or a normal form submit, the JavaScript marks those sections as invalid and reloads them.
  • Lifetime: each section expires after the Customer Data Lifetime set under Stores, Configuration, Customers, Customer Configuration, Online Customers Options. The config path is customer/online_customers/section_data_lifetime, in minutes.
  • The private_content_version cookie: Magento writes a new value to this cookie when a POST request reaches the server, which covers login, logout and most actions that change private data. A changed value tells the browser its cached sections are out of date.
  • Manual: your JavaScript can call customerData.reload() or customerData.invalidate().

Adding a custom section

As an example, a section with a free shipping progress message that depends on the current cart. It needs three parts: a section source class, a DI registration and a sections.xml file.

1. The section source

<?php

declare(strict_types=1);

namespace Vendor\FreeShipping\CustomerData;

use Magento\Checkout\Model\Session as CheckoutSession;
use Magento\Customer\CustomerData\SectionSourceInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;

class FreeShippingProgress implements SectionSourceInterface
{
    public function __construct(
        private readonly CheckoutSession $checkoutSession,
        private readonly ScopeConfigInterface $scopeConfig
    ) {
    }

    public function getSectionData(): array
    {
        $threshold = (float) $this->scopeConfig->getValue(
            'carriers/freeshipping/free_shipping_subtotal',
            ScopeInterface::SCOPE_STORE
        );
        $subtotal = (float) $this->checkoutSession->getQuote()->getBaseSubtotalWithDiscount();

        return [
            'threshold' => $threshold,
            'remaining' => max(0.0, $threshold - $subtotal),
        ];
    }
}

Keep this class fast. It runs on every section load request for every visitor who triggers one, and customer/section/load is not cached. Heavy queries or external API calls here will show up as slow add to cart responses.

2. Register it in etc/frontend/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Customer\CustomerData\SectionPoolInterface">
        <arguments>
            <argument name="sectionSourceMap" xsi:type="array">
                <item name="free-shipping-progress" xsi:type="string">Vendor\FreeShipping\CustomerData\FreeShippingProgress</item>
            </argument>
        </arguments>
    </type>
</config>

3. Invalidate it in etc/frontend/sections.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd">
    <action name="checkout/cart/add">
        <section name="free-shipping-progress"/>
    </action>
    <action name="checkout/cart/delete">
        <section name="free-shipping-progress"/>
    </action>
    <action name="checkout/cart/updatePost">
        <section name="free-shipping-progress"/>
    </action>
    <action name="checkout/sidebar/removeItem">
        <section name="free-shipping-progress"/>
    </action>
    <action name="checkout/sidebar/updateItemQty">
        <section name="free-shipping-progress"/>
    </action>
</config>

The action names are route, controller and action paths. Missing one is the usual reason a section shows stale data after a particular user action. Look at Magento_Checkout/etc/frontend/sections.xml to see which actions core uses for the cart section, and mirror them.

4. Use it in JavaScript

define([
    'uiComponent',
    'Magento_Customer/js/customer-data'
], function (Component, customerData) {
    'use strict';

    return Component.extend({
        defaults: {
            template: 'Vendor_FreeShipping/progress'
        },

        initialize: function () {
            this._super();
            this.progress = customerData.get('free-shipping-progress');

            return this;
        }
    });
});

customerData.get() returns a Knockout observable, so the template reads it as progress().remaining and updates when the section reloads. If you need to react to changes in code, subscribe to it with this.progress.subscribe(callback).

The template and the text/x-magento-init setup follow the same pattern as any other storefront component. The UI components and Knockout basics article covers that part.

Common mistakes

Using cacheable="false" instead

Marking a block cacheable="false" in layout XML disables full page cache for the whole page, not just the block. On a default.xml handle it disables it for the whole store. If a page has personal data in it, move that data to a section and keep the page cacheable. The Varnish full page cache article shows how to check hit rates after the change.

Putting customer data in a block and hoping for the best

Older code sometimes reads the customer session inside a block that ends up in cached HTML. On a cached page, the session in that block is empty, because Magento clears customer data from the session during rendering of cacheable pages. The block shows guest content for everybody, or worse, content from whoever first warmed the cache if the data came from somewhere else.

Forcing a reload on every page

Calling customerData.reload(['cart'], true) on every page load defeats local storage caching and adds an uncached request to every view. Reload in response to actions, not on page load.

Large sections

Section data is serialised into local storage and sent to every page. Do not return full product collections or HTML snippets. Return the minimum the component needs to render.

Using a section for data that is not private

The opposite mistake also happens. A store loads a list of banners or a store wide notice through a custom section because it seemed easy. That data is the same for every visitor, so it belongs in the cached HTML, or in a block with a proper cache key. Moving it into a section adds an uncached request and a flash of empty content for no benefit. Sections are for data that differs per visitor, nothing else.

Debugging

  • Check the Network tab for customer/section/load requests and see which sections parameter they send.
  • Inspect mage-cache-storage in local storage to see the current values and timestamps.
  • In the console, require('Magento_Customer/js/customer-data').get('cart')() prints the current cart section.
  • To test from a clean state, clear local storage for the domain and reload.

For more on which caches hold what, see Magento 2 cache types explained.

FAQ

Why does the minicart show old items after an action?

The controller that changed the cart is not mapped to the cart section in any sections.xml, so the browser keeps the old copy in local storage. Add an action entry for that controller path.

Is section data secure?

It is loaded for the current session only and stored in the visitor's own browser. Still, treat it as visible to the user and do not include anything they should not see, such as internal cost data.

Can I use sections for logged in pricing?

Prices by customer group are handled by Magento through the customer context in full page cache, which creates separate cached variants per group. Use sections for data that is truly per customer, not per group.

Do customer data sections exist in Hyvä?

Yes. Hyvä keeps the server side sections and customer/section/load, and replaces the RequireJS client with its own lightweight JavaScript that dispatches an event when private content loads.

I am available for freelance Magento and Adobe Commerce work, including full page cache and performance fixes. If you need help with this, get in touch.

Related articles