SCAPI Caching
SCAPI caches eligible GET responses in a two-tier cache to reduce latency and load on the origin. A request passes through the tiers in the following order:
- Web-tier cache (origin): One of the B2C Commerce application layer components performs web-tier caching of SCAPI requests, which affects GET requests to various API families. This cache runs only after a request reaches the B2C Commerce server-side application layer. The web-tier cache is the foundation of SCAPI caching and determines what is cacheable and for how long.
- CDN cache (edge): An implicit Content Delivery Network (CDN) cache sits in front of the web-tier cache. It automatically caches eligible, non-personalized responses at the edge, closer to the shopper, so that repeat requests are answered without contacting the origin. For these responses, the CDN mirrors the web tier: their time-to-live and invalidation follow the web-tier behavior described on this page.
If there is another caching mechanism in place (for example, on the client side), it is still used independently. Consider this when dealing with multiple layers of caching on the front-end or client side of a storefront implementation.
The SCAPI CDN cache is distinct from the eCDN edge caching used by storefronts.
Responses to GET requests of the following APIs are cached:
- /product/shopper-products/v1/organizations/{org-id}/categories/{id}
- /product/shopper-products/v1/organizations/{org-id}/categories
- /product/shopper-products/v1/organizations/{org-id}/products/{id}
- /product/shopper-products/v1/organizations/{org-id}/products
- /product/shopper-availability/v1/organizations/{org-id}/availability
- /search/shopper-search/v1/organizations/{org-id}/product-search
- /search/shopper-search/v1/organizations/{org-id}/search-suggestions
- /pricing/shopper-promotions/v1/organizations/{org-id}/promotions/campaigns
- /pricing/shopper-promotions/v1/organizations/{org-id}/promotions/campaigns
- /site/shopper-seo/v1/organizations/{org-id}/url-mapping
- /store/shopper-stores/v1/organizations/{org-id}/stores
- /configuration/preferences/v1/organizations/{org-id}/site-custom-preferences?{siteId}
- /configuration/preferences/v1/organizations/{org-id}/global-custom-preferences
- /experience/shopper-experience/v1/organizations/{org-id}/pages
- /experience/shopper-experience/v1/organizations/{org-id}/pages/{pageId}
For Shopper Experience API caching details, see the Page Designer documentation.
- /configuration/shopper-configurations/v1/organizations/{org-id}/configurations
The following table summarizes the caching status of all Shopper APIs:
| API | Cached | Notes |
|---|---|---|
| Shopper Products (categories, products) | Yes | TTL varies by expansion (60s-86400s) |
| Shopper Search (product search, suggestions) | Yes | TTL varies by expansion (60s-86400s) |
| Shopper Availability | Yes | 60s TTL |
| Shopper Promotions (promotions, campaigns) | Yes | 3600s TTL |
| Shopper SEO (URL mapping) | Yes | 43200s TTL |
| Shopper Stores | Yes | 86400s TTL |
| Shopper Experience (Page Designer) | Yes | See Page Designer caching |
| Shopper Configurations | Yes | 900s TTL |
| Preferences (Admin, site and global) | Yes | 300s TTL |
| Shopper Baskets | No | Stateful/transactional - each basket is unique |
| Shopper Orders | No | Stateful/transactional |
| Shopper Customers | No | Contains personal data |
| Shopper Login (SLAS) | No | Authentication endpoints |
| Shopper Payments | No | Payment configuration |
| Shopper Context | No | Per-shopper state |
| Shopper Custom Objects | No | Dynamic data |
| Shopper Consents | No | Per-shopper state |
| Shopper Gift Certificates | No | Transactional (POST-based lookup) |
| Custom APIs | See Custom API Caching |
Page caching of dynamic content must be enabled for the specific site.
To do so, in Business Manager, navigate to Administration > Sites > Manage Sites > Site Name - Cache and select “Enable Page Caching”. This single setting governs both the web-tier cache and the CDN cache.
How long a cache entry lives and whether the response is personalized, depends on the API and, if there are expansions available on the API, the selected expansions.
| API name | Expansion | Cache time-to-live, seconds (Defaults) | Is personalized (Defaults) |
|---|---|---|---|
| Categories | N/A | 86400 | No |
| Products | availability | 60 | No |
| bundled_products | 86400 | No | |
| images | 86400 | No | |
| links | 86400 | No | |
| options | 86400 | No | |
| page_meta_tags | 86400 | No | |
| prices | 900 | Yes | |
| promotions | 900 | Yes | |
| recommendations | 86400 | No | |
| set_products | 86400 | No | |
| shipping_methods | 86400 | No | |
| variations | 86400 | No | |
| none | 86400 | No | |
| Availability | N/A | 60 | No |
| Product Search | availability | 60 | No |
| custom_properties | 86400 | No | |
| description | 86400 | No | |
| images | 86400 | No | |
| page_meta_tags | 86400 | No | |
| prices | 900 | Yes | |
| primary_category | 86400 | No | |
| promotions | 900 | Yes | |
| represented_products | 86400 | No | |
| slug | 86400 | No | |
| variations | 86400 | No | |
| none | 86400 | No | |
| Search Suggestions | N/A | 900 | No |
| Promotions | N/A | 3600 | No |
| Campaigns | N/A | 3600 | No |
| SEO Url Mapping | N/A | 43200 | No |
| Stores | N/A | 86400 | No |
| Configurations | N/A | 900 | No |
| Preferences (Site) | 300 | No | |
| Preferences (Global) | 300 | No |
The lowest time-to-live of all the expansions decides how long the cache entry stays alive. If at least one of the requested expansions is marked as “personalized”, the whole response is considered to be personalized.
The cache hit rates are greatly impacted by the expansions you select for the product and product-search endpoints. For example, if a request to the product API includes the “availability” expansion, the cache entry is stored for only 60 seconds by default. If no expand parameter is specified, all expansions are considered selected.
Example 1
A request to /product/shopper-products/v1/organizations/{org-id}/products?ids=product1,product2 is made every 30 minutes. Since no expand parameter is specified, all expansions are applied, and the lowest time-to-live among them is for “availability”, which is 60 seconds. Consequently, the cache hit rate is likely to remain zero because the cache entry expires before the next identical request is made.
Example 2
A request to /product/shopper-products/v1/organizations/{org-id}/products?ids=product1,product2&expand=images,links,options is made every hour. The shortest time-to-live among the specified expansions is 86400 seconds (24 hours), so the cache hit rate is expected to be high.
Due to how short the cache entry live time is for certain expansions, we recommend you make sure only necessary expansions are included in the request. For example, unnecessarily including the “availability” expansion in requests to the Products and Product Search APIs reduces the cache hit rate drastically and negatively affect the overall performance of requests. If no expand parameter is specified, all expansions are considered selected.
To get more requests served from cache:
- Request only the expansions you need, and avoid low-TTL expansions such as
availabilityon requests you want cached longer. See “expand” Parameter Impact on Cache Hit Rates. - Avoid hook-based personalization. Apply personalization before the cache key is computed, through the Shopper Context API. See Personalization and Custom Code.
- Avoid unnecessary custom query parameters, because each distinct query string is a separate cache entry.
- Track the effect of these changes with cache statistics.
When personalization is enabled for a resource, the following information becomes part of the cache key in addition to the URL string:
- The complete set of active promotions.
- The complete set of active product sorting rules.
- The complete set of applicable price books.
- The complete set of active ABTest Groups.
The cache stores different response variations in the cache and delivers the correct version to the API user based on this additional information.
Let’s think of what it means for two shoppers accessing the same product API (as in, the same URL). In this case, shopper A is eligible for promotion X, and shopper B is eligible for promotion Y. The same product (no change in URL) is cached twice. All shoppers with the promotion X are subsequently served the same respective cache entry, as are the shoppers with the promotion Y. Depending on the number of price books and promotions, this scenario can lead to a large increase in the number of cache entries, regardless of the price of the product.
Consider using personalized caching only when necessary. And only for well-sized groups of shoppers.
The personalized=none query parameter marks a request as non-personalized. When it is present, the B2C Commerce application skips applying personalization to the request, including the Shopper Context, and the first computed response is reused for all subsequent shoppers of that request.
Because non-personalized responses are cacheable for a wider audience, using personalized=none where the content is intended to be identical for all shoppers can improve cache efficiency at both the web-tier and CDN layers.
personalized=none can be applied to any Shopper API except Shopper Login (SLAS) and Shopper Context. It takes precedence over custom code: if a hook marks the response as personalized with dw.system.Response#setVaryBy(identifier), that call is suppressed and the response is still treated as non-personalized.
Use personalized=none only for content that is intended to be identical for all shoppers.
For a new request, the web-tier first checks for the existence of a cache entry. The cache key is calculated before any hook customization is invoked, and doesn’t include any changes that occur later within hooks.
Personalization changes made within hook logic aren’t compatible with Server-Side Web-Tier Caching.
The cache key calculation happens before any hook customization is executed, which means that any personalization changes made within hooks, such as modifying price books, promotions, or other personalization details, aren’t considered during the cache key generation process.
Step-by-Step Example:
- Request arrives: A shopper requests a product with promotions expansion.
- Cache key calculation: The system calculates the cache key based on:
- URL and query parameters
- Other request-related factors
- Current active promotions
- Current applied price books
- Current product sorting rules
- Current AB test groups
- Cache lookup: The system looks for an existing cache entry using the cache key.
- If there is a cache miss: The request proceeds to processing.
- Hook execution: Custom hooks are invoked and modify personalization details.
- Response generation: The final response is generated with a potentially different personalization.
- Cache storage: The response is stored with the altered cache key that reflects the changes in the personalization.
The cache key from step 2 doesn’t reflect the personalization changes made in step 5, leading to:
- Incorrect cache entries being served to users
- Cache misses
- Potential performance degradation due to frequent cache misses
Changing applicable price books and promotions within hook logic causes the final cache entry to not match the original key. This causes a decrease in performance due to a lower cache hit rate. Use the Shopper Context API to apply personalization changes before the cache key calculation occurs, instead of relying on hook-based personalization modifications.
Potential updates of the response body, such as adding custom attributes, aren’t considered within the cache key. If two otherwise identical requests (URL and query string) are supposed to produce different responses solely due to conditional hook logic, the web-tier considers such requests as identical and returns the cached response. To make sure that the web-tier cache behaves correctly, append a custom query parameter to the URL.
Example:
The customization of responses via conditional code is only cached correctly in case the condition is given as part of the URL that is, a custom query parameter. Do this only when necessary, because it can impact performance.
With B2C Commerce 24.8, set a time window, called the Validity Period, for the Shopper Products getProduct and getCategory endpoints and the Campaigns getCampaign endpoint. The validity is considered for server-side cache expiration in order to prevent serving stale content. The attributes that define the Validity Period are Valid From and Valid To. For other resource types, the default cache expiration times are applied.
The server-side cache expiration is adjusted to reflect date and time values of Valid From and Valid To attributes if those are defined and take effect prior to the default cache expiration. Otherwise, the default expiration is applied.
For requests that contain multiple resource identifiers, such as multiple SKUs, all Validity Periods are taken into consideration when calculating cache expiration. The earliest effective date determines the cache expiration of the response.
For example, a request using a single Product SKU is cached by default for 24 hours (no additional expansions; see table above). Assuming the Product has an assigned Valid To time that is earlier than the default cache expiration, then the cache expiration is recalculated to reflect the shorter Valid To time.

If you use a Valid From time to configure a Product launch for a future date, an HTTP 404 response code is returned. The HTTP 404 response is also cached using a cache expiration that reflects the respective Valid From value. When the cache expires at that time, the Product becomes viewable for shoppers in your storefront.

Script API provides options to control the cache settings programmatically.
The dw.system.Response#setExpires( milliseconds ) method of the Script API makes it possible to set an arbitrary cache expiration timestamp. Since the method accepts the timestamp in milliseconds, a sum of the current timestamp and desired “time-to-live” has to be passed as a parameter.
For instance, if a response for the Category API should be only cached for an hour instead of the day, the following custom code can be used:
The cache time-to-live has to be at least one second and cannot exceed 86,400 seconds.
A TTL set with setExpires applies to the CDN cache entry as well; the CDN mirrors the web-tier TTL.
The dw.system.Response#setVaryBy( String varyBy ) method of the Script API marks the response as personalized with the given variant identifier. Only price_promotion is supported, and any other value has no effect.
By default, product calls with prices and promotions expansions, as well as product search calls with the prices and promotions expansions, are personalized.
Cache invalidation is kept in sync across both cache tiers: clearing the web-tier page cache also purges the corresponding CDN entries.
At the moment there is only one way to invalidate cache manually - which is to invalidate the entire site page cache.
In Business Manager, navigate to Administration > Sites > Manage Sites > Site Name - Cache, and select the Cache tab. In the “Cache Invalidation” section there is a button to invalidate the site page cache. Once the invalidation has been triggered, the entirety of the site page cache is cleared within 15 minutes, including the cache related to SCAPI responses. The corresponding CDN entries are purged as well.
Doing this also invalidates all existing pipeline cache, possibly resulting in a temporary decrease in performance.
A data replication that clears a site’s page cache also purges the site’s CDN entries automatically, keeping both cache tiers consistent with the newly published data.
As with the manual case, invalidation is not instantaneous: the clear is staggered over an interval (approximately 15 minutes) to avoid a sudden load spike on the origin.
Purge-by-tag drives CDN invalidation. Every cacheable SCAPI response carries a Cache-Tag response header with two tags:
pc:<siteId>_<pageCacheId>: identifies the site’s current page cache generation (site ID plus page cache ID). When the page cache is cleared, the page cache ID changes and the entries tagged with the previous value are purged.site:<siteId>: identifies the site, so all of a site’s CDN entries can be purged together.
With the new server-side caching in place, responses of cached endpoints always return the Cache-Control header value: no-cache, no-store, must-revalidate. This is necessary to maintain uniform behavior of SCAPI and Script Controller APIs, especially in the case of personalization.
SCAPI returns a separate cache-status response header for each cache tier: sfdc_cache_status for the web tier and cf-cache-status for the CDN edge.
The sfdc_cache_status response header provides visibility into how the web-tier cache resolved a request.
The sfdc_cache_status header is calculated and assigned in the web tier and is returned for:
- Shopper API endpoints listed in the What Is Cached section of this page.
- Custom APIs.
The sfdc_cache_status header returns one of the following values:
| Value | Description | Example |
|---|---|---|
HIT [M/M] | Main request plus all related include requests were fully resolved from cache. | HIT [1/1] |
MISS [0/M] | No requests were resolved from cache. | MISS [0/1] |
PARTIAL [N/M] | Request was partially resolved from cache. N is greater than 0 but less than M. | PARTIAL [2/5] |
The notation [N/M] provides insight into cache efficiency:
- M = Total number of requests, including the main request and all related include requests.
- N = Number of requests that were resolved from the cache.
Because most requests don’t involve include requests, M usually equals 1, making HIT [1/1] and MISS [0/1] the most common values. The PARTIAL status only appears if include requests are involved.
For example, if a request involves include requests and the response header shows PARTIAL [3/5], it means:
- Five total requests were made (one main request + four include requests).
- Three of the five requests were served from cache.
- Two requests resulted in cache misses and were processed by the application.
Example:
sfdc_cache_status reflects the web tier only.
The cf-cache-status response header reports how the CDN layer resolved the request. It reflects only the CDN tier.
| Value | Description |
|---|---|
HIT | The response was served from the CDN cache. |
MISS | The response was not in the CDN cache; it was fetched from the origin and may now be cached. |
EXPIRED | The resource was found in the CDN cache but had expired, so it was served from the origin. |
UPDATING | The resource had expired but was served from the CDN cache while the origin refreshed it in the background. |
DYNAMIC | The request was not eligible for CDN caching and was served from the origin. |
BYPASS | The response could not be cached in the CDN because it was personalized. |
Consider a shopper GET request for an eligible, non-personalized resource.
First request (both tiers empty):
- The CDN has no entry, so
cf-cache-statusisMISS. The request is forwarded to the origin. - The web-tier cache also has no entry, so
sfdc_cache_statusisMISS [0/1]. The B2C Commerce application computes the response. - The response is stored in the web-tier cache and in the CDN edge cache.
Subsequent identical request (served from the edge):
The response is returned directly from the CDN, and the origin is not contacted:
Because the request never reaches the origin on a CDN hit, the web tier does not process it and sfdc_cache_status is not recalculated for that request.
It is normal to observe cf-cache-status: HIT together with sfdc_cache_status: MISS on subsequent responses. When a request is computed for the first time, its response carries sfdc_cache_status: MISS (nothing was cached in the web tier yet) and is then stored in both the web-tier cache and the CDN. Because that stored copy already includes sfdc_cache_status: MISS, later requests resolved from the CDN return it verbatim without traveling to the web tier. This combination reflects a CDN hit on the originally computed response and is not an indicator of a problem in the web-tier cache.
Aggregate SCAPI cache statistics are available in Reports & Dashboards:
- In Business Manager, click App Launcher and select Merchant Tools > select your site.
- Go to Analytics > Reports and Dashboards > Technical > SCAPI.
These statistics reflect web-tier cache behavior.
If a personalized response is served incorrectly - for example, two requests that should receive different personalization return the same cached response - work through the following steps:
- Review Personalization and Custom Code and confirm your implementation follows it. In particular, the cache key is computed before hooks run, so personalization changes made in hooks aren’t reflected in the cache key.
- If the behavior persists, reproduce the requests with debug logging enabled by sending the
sfdc_verbose: truerequest header. See Collect Detailed Request Information. - In the logs, inspect the cache status response headers together with the debug log entries that begin with
Price promotion personalization hash. These entries record the personalization inputs (promotions, price books, product sorting rules, and AB test groups) that make up the cache key. - Compare two requests that are expected to have different personalization. If their logged personalization inputs - and therefore their cache key - are identical, the requests resolve to the same cache entry and receive the same personalization. This indicates the differing personalization is not being applied before the cache key is computed (for example, it is applied in a hook). Apply it earlier instead, such as through the Shopper Context API.
Logout invalidates tokens at the origin. On a CDN edge cache hit, the origin is never contacted, so a previously cached 200 response can still be served after logout instead of a 401. Only responses served from the CDN cache are affected.
Don’t include personally identifiable information (PII) in responses that can be cached, including custom attributes and Custom API responses. As long as a cached response contains no PII, stale delivery after logout has no security impact.
Use the Web Adapter Cache Key Ignore by Query String feature toggle to exclude query parameters from the cache key. This applies when you want to increase the cache hit rate, but only when the query parameter does NOT influence the response in any way. Because the specified query parameters affect the caching of Salesforce Commerce APIs and Controllers, use this feature toggle with caution.