Cache - Liquid Tag Reference
On this page
The cache block tag stores rendered output in the server-side cache so it can be reused without re-processing. Wrap expensive queries, loops, or external calls in {% cache %}...{% endcache %} to cut page render time.
Every cache block needs a cache key as its first argument. A block with no key raises an error when the template renders.
Syntax
```liquid
{% cache “
| Property | Value |
|---|---|
| Tag Name | cache |
| Type | Block tag |
| Source | Hydrofoil (core) |
Parameters
| Parameter | Type | Description |
|---|---|---|
| Cache key | String | Required, passed as the first argument with no parameter name. A short name for the block, or a variable holding one. Rendering fails if it is missing or empty. |
items |
Array | The objects the block’s output depends on. Each one is added to the cache key so every product, customer, or cart gets its own cached copy. |
expires_in |
Number | How long the cached output lives, in seconds. Defaults to 60 seconds. |
race_condition_ttl |
Number | How long, in seconds, the expired copy keeps being served while a new one is generated. Defaults to 5 seconds. When expires_in is 5 seconds or less, this becomes half of expires_in. |
force |
Boolean | Set true to skip the existing entry and rewrite it. Useful while developing. |
skip_nil |
Boolean | Set true to leave the cache untouched when the block produces nothing. |
How the cache key is built
The final key combines the name you pass, the current store, and each entry in items. Two things follow from this:
- Keys are isolated automatically. The same cache name in two different stores, or in two different Salesforce orgs, never collides. You do not need to add the store to the key yourself.
itemsscopes the key, it mostly does not watch for edits. Most objects contribute only their record ID, so passingproductgives that product its own cached copy, but editing the product does not clear it.current_cartis the exception: it contributes a version that moves whenever the cart changes, so a block keyed on the cart refreshes as soon as the cart does.- Expiry is the only general way out. A
cacheblock refreshes whenexpires_inelapses. There is no way to clear a single block on demand, so chooseexpires_into match how quickly the content has to be correct.
Changing the Cache Version field on the Store record in Salesforce, which publishing a content change does for you, clears the store’s page cache. It does not reach inside cache blocks: those are keyed separately and keep serving their copy until it expires.
:::note Caching is bypassed while you preview an unpublished content change in Studio, so the editor always shows current content. A change that looks correct in the editor can still serve a cached copy on the live store until the block expires. See changes not showing on the website. :::
Examples
Cache a product card
Pass the product so each one is cached separately, and the store and customer so that price, availability, and customer-specific text stay correct:
```liquid
{%- cache “product”, items: [product, current_store, current_customer] -%}
{{ product.name }}
{{ product.pricing.price | money }} {%- render “products/product/add_to_cart”, product: product -%} {%- endcache -%} ```
Cache the header
The header shows the cart item count, so include current_cart. Without it, a customer’s own header would keep the count it had when the block was cached, and signed-out visitors, who share a single key, would see each other’s count:
```liquid
{%- cache “header”, items: [current_store, current_customer, current_cart] -%} {%- render “menu” -%} {{ current_cart.item_count }} {%- endcache -%} ```
Build the key from a variable, and hold it longer
Where a name alone is not specific enough, build the key first and pass the variable. Content that changes rarely can be held well beyond the default minute, so this related-products list is cached for ten minutes:
```liquid
{%- capture related_key %}related/{{ product.id }}{% endcapture -%} {%- cache related_key, items: [current_customer], expires_in: 600 -%} {%- for related in product.related_products -%} {%- render “products/card”, product: related -%} {%- endfor -%} {%- endcache -%} ```
Cache an external call
Wrap an api block so the storefront does not call the service on every render:
```liquid
{%- cache “forecast”, items: [current_store], expires_in: 900 -%} {% api url: ‘https://api.example.com/v1/forecast’, method: ‘GET’, async: false %} {% if response.status == 200 %}{{ response.body.summary }}{% endif %} {% endapi %} {%- endcache -%} ```
Additional recommendations
- Always pass the objects the output depends on. The most common caching bug is omitting
current_customerorcurrent_cartfrom a block whose output differs per customer, which makes one customer’s rendered output visible to everyone until it expires. - Never cache a block containing a
formtag. Forms include a hidden security token tied to the visitor’s session, so a cached copy hands the next visitor a token that fails validation on submit. The same applies to anything else built from session values. - Cache the expensive part, not the whole page. Small blocks around slow queries and external calls give most of the benefit and stay correct for longer.
- Measure before and after. Wrap the block in a
timerblock to confirm it is worth caching. - Keep expiry short while developing, or pass
force: true, so you are not debugging output that a cached copy is producing.
:::warning
Prices, stock levels, and cart contents stay stale for as long as the cached copy lives. Keep expires_in short on anything a customer makes a purchase decision from, and always include current_customer in items so that customer-specific pricing, approvals, and membership discounts are not shared between visitors.
:::
Was this article helpful?
Thanks for your feedback! It helps us improve our docs.