Magento’s built-in Full Page Cache stores pages in files or Redis. It works, but it handles requests inside PHP – nginx and PHP-FPM still have to start for every request. Varnish sits in front of nginx and serves pages from memory without touching PHP. For a shop with traffic that is the difference between a second and milliseconds.
How Varnish works with Magento 2
Stack without Varnish: Browser – nginx – PHP-FPM – Magento – Redis/file cache
With Varnish: Browser – Varnish – (cache HIT: immediate response) or (cache MISS: nginx – PHP-FPM – Magento)
Varnish holds pages in RAM. A cache HIT is literally a few milliseconds – Varnish fires no PHP process. A cache MISS goes to the backend (nginx + PHP), which generates the page and hands it to Varnish for caching.
Configuring Magento for Varnish
# Enable Varnish as the FPC engine
bin/magento config:set system/full_page_cache/caching_application 2
# Generate the VCL configuration file for Varnish
bin/magento varnish:vcl:generate \
--backend-host=nginx \
--backend-port=80 \
--export-version=6 \
--output-file=/etc/varnish/magento.vcl
VCL basics – how Varnish makes decisions
# Fragment of magento.vcl - simplified for readability
sub vcl_recv {
# Do not cache requests from logged-in customers
if (req.http.cookie ~ "PHPSESSID" && req.http.cookie ~ "customer_logged_in") {
return (pass);
}
# Do not cache POST requests
if (req.method == "POST") {
return (pass);
}
# Normalise URL - remove tracking parameters
if (req.url ~ "(\?|&)(utm_source|utm_medium|utm_campaign|gclid)=") {
set req.url = regsuball(req.url, "&(utm_source|utm_medium|utm_campaign|gclid)=[^&]+", "");
}
return (hash);
}
sub vcl_backend_response {
# Cache product page for 1 hour
if (bereq.url ~ "^/catalog/product/view") {
set beresp.ttl = 1h;
}
# Home page - 5 minutes
if (bereq.url == "/") {
set beresp.ttl = 5m;
}
}
Cache tagging and invalidation
Magento tags every page with an X-Magento-Tags header containing identifiers of the entities used on that page. When a product changes, Magento sends a PURGE request to Varnish with that product’s tag – Varnish removes from cache all pages containing that tag:
<?php
namespace Vendor\Module\Model;
use Magento\CacheInvalidate\Model\PurgeCache;
use Magento\Framework\App\Cache\Tag\Resolver;
class ProductCacheInvalidator
{
public function __construct(
private PurgeCache $purgeCache,
private Resolver $tagResolver
) {}
public function invalidateProduct(\Magento\Catalog\Model\Product $product): void
{
$tags = $this->tagResolver->getTags($product);
$this->purgeCache->sendPurgeRequest(implode('|', $tags));
}
}
ESI – dynamic blocks on cached pages
Problem: the product page is cached, but the “Recently viewed” block is per-user. ESI (Edge Side Includes) solves this through separate caching of blocks:
<!-- Varnish replaces this tag with a separate backend request --> <esi:include src="/page_cache/block/esi/blocks/" />
Varnish in DDEV
# .ddev/docker-compose.varnish.yaml
version: '3.6'
services:
varnish:
image: varnish:6.4
ports:
- "6081:6081"
volumes:
- ./magento.vcl:/etc/varnish/default.vcl
environment:
- VARNISH_SIZE=256m
depends_on: [web]
labels:
com.ddev.site-name: ${DDEV_SITENAME}
# Varnish statistics varnishstat # Real-time logs varnishlog -q "ReqURL ~ '/catalog/product'" # Manual PURGE of entire cache curl -X PURGE http://localhost:6081/.* # Check if a page came from Varnish curl -I https://magento2-dev.ddev.site/ | grep X-Cache # X-Cache: HIT or MISS
Summary
Varnish is one of the biggest performance improvements you can make for a Magento shop under load. A cache HIT eliminates PHP from handling the request entirely. The key is a good understanding of VCL and the tagging mechanism – then you can precisely control what is cached and for how long, without the risk of serving stale data.
