Custom Varnish VCL rules development

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Custom Varnish VCL rules development
Complex
~2-3 business days
FAQ
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1218
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    853
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1047
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    819

Developing Custom Varnish VCL Rules

Varnish Cache without custom VCL is a hammer trying to drive screws. Default configuration caches everything or nothing, ignores auth headers, can't handle mobile site versions, and crashes on the first edge case with cookies.

Custom VCL rules solve specific tasks: cache management by conditions, request normalization, CDN bypass for certain routes, routing to different backends, grace mode when origin is down.

VCL Architecture and Entry Points

VCL (Varnish Configuration Language) is a domain-specific language with a state machine: vcl_recv, vcl_hash, vcl_hit, vcl_miss, vcl_pass, vcl_fetch (Varnish 4+: vcl_backend_fetch/vcl_backend_response), vcl_deliver.

Typical starting point for customization—vcl_recv. Here we decide what to cache, what to bypass, what to serve from cache without hitting the backend.

vcl 4.1;

import std;
import directors;

backend default {
    .host = "127.0.0.1";
    .port = "8080";
    .connect_timeout = 2s;
    .first_byte_timeout = 60s;
    .between_bytes_timeout = 10s;
    .probe = {
        .url = "/healthz";
        .timeout = 1s;
        .interval = 5s;
        .window = 5;
        .threshold = 3;
    }
}

backend api {
    .host = "127.0.0.1";
    .port = "8081";
    .connect_timeout = 1s;
    .first_byte_timeout = 30s;
}

Request Normalization

Without normalization, the same resource is stored under dozens of keys: /?utm_source=google, /?utm_source=facebook, /?fbclid=abc123—different cache entries for identical content.

sub vcl_recv {
    # Remove marketing parameters before building cache key
    if (req.url ~ "(\?|&)(utm_source|utm_medium|utm_campaign|utm_term|utm_content|fbclid|gclid|yclid|_ga|mc_eid)=") {
        set req.url = regsuball(req.url, "&(utm_source|utm_medium|utm_campaign|utm_term|utm_content|fbclid|gclid|yclid|_ga|mc_eid)=[^&]*", "");
        set req.url = regsuball(req.url, "\?(utm_source|utm_medium|utm_campaign|utm_term|utm_content|fbclid|gclid|yclid|_ga|mc_eid)=[^&]*&?", "?");
        set req.url = regsub(req.url, "\?$", "");
    }

    # Normalize Accept-Encoding—Varnish stores separate objects for gzip/br/identity
    if (req.http.Accept-Encoding) {
        if (req.url ~ "\.(jpg|jpeg|png|gif|webp|gz|tgz|bz2|tbz|mp3|ogg|swf|flv|mp4|woff2?)$") {
            unset req.http.Accept-Encoding;
        } elsif (req.http.Accept-Encoding ~ "br") {
            set req.http.Accept-Encoding = "br";
        } elsif (req.http.Accept-Encoding ~ "gzip") {
            set req.http.Accept-Encoding = "gzip";
        } else {
            unset req.http.Accept-Encoding;
        }
    }

    # Normalize cookies—keep only needed for auth
    if (req.http.Cookie) {
        set req.http.Cookie = ";" + req.http.Cookie;
        set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
        set req.http.Cookie = regsuball(req.http.Cookie, ";(session|auth_token|XSRF-TOKEN)=", "; \1=");
        set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");
        if (req.http.Cookie == "") {
            unset req.http.Cookie;
        }
    }
}

Routing by Request Type

API and static assets need different logic. API—short TTL or pass, static—long TTL with normalization.

sub vcl_recv {
    # API—don't cache, pass directly
    if (req.url ~ "^/api/") {
        return(pass);
    }

    # Route to separate backend for API
    if (req.url ~ "^/api/v[0-9]+/") {
        set req.backend_hint = api;
        return(pass);
    }

    # Authorized users—personal content, don't cache
    if (req.http.Authorization || req.http.Cookie ~ "auth_token=") {
        return(pass);
    }

    # POST, PUT, DELETE, PATCH—don't cache
    if (req.method != "GET" && req.method != "HEAD") {
        return(pass);
    }

    # Static—cache aggressively, remove cookies
    if (req.url ~ "\.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot|webp|avif)(\?.*)?$") {
        unset req.http.Cookie;
        return(hash);
    }

    return(hash);
}

Grace Mode and Stale-While-Revalidate

Grace allows serving stale cache while backend is overloaded or temporarily down. Critical for high-load sites.

sub vcl_backend_response {
    # Set grace period: 24 hours after TTL expiration
    set beresp.grace = 24h;

    # If backend returned 5xx—save object for grace
    if (beresp.status >= 500) {
        set beresp.ttl = 0s;
        set beresp.grace = 60s;
        return(deliver);
    }

    # Custom TTL by content type
    if (bereq.url ~ "^/news/") {
        set beresp.ttl = 10m;
    } elsif (bereq.url ~ "^/static/") {
        set beresp.ttl = 30d;
        unset beresp.http.Set-Cookie;
    } elsif (bereq.url ~ "^/product/") {
        set beresp.ttl = 1h;
    } else {
        set beresp.ttl = 5m;
    }

    # Don't cache if backend explicitly forbids
    if (beresp.http.Cache-Control ~ "no-store|private") {
        set beresp.uncacheable = true;
        return(deliver);
    }
}

sub vcl_hit {
    # If object is stale but grace allows—serve and revalidate background
    if (obj.ttl >= 0s) {
        return(deliver);
    }
    if (obj.ttl + obj.grace > 0s) {
        return(deliver);
    }
    return(restart);
}

Cache Invalidation

Purge by tags (via xkey/surrogate keys)—the right approach for CMS with object dependencies.

import xkey;

sub vcl_recv {
    # Simple purge by URL
    if (req.method == "PURGE") {
        if (!client.ip ~ purge_acl) {
            return(synth(405, "Not allowed"));
        }
        return(purge);
    }

    # Soft-purge by tag (xkey)
    if (req.method == "XKEY-PURGE") {
        if (!client.ip ~ purge_acl) {
            return(synth(405, "Not allowed"));
        }
        set req.http.n-gone = xkey.softpurge(req.http.xkey-purge);
        return(synth(200, "Purged " + req.http.n-gone + " objects"));
    }
}

sub vcl_backend_response {
    # Backend sends surrogate keys in header
    if (beresp.http.Surrogate-Key) {
        set beresp.http.xkey = beresp.http.Surrogate-Key;
    }
}

Invalidation call from application:

# Purge specific page
curl -X PURGE https://example.com/news/article-123

# Purge by tag (all pages related to category)
curl -X XKEY-PURGE -H "xkey-purge: category:5" https://example.com/

Device Variants

If mobile version is served from the same URL, store separate objects for desktop and mobile.

sub vcl_hash {
    hash_data(req.url);
    if (req.http.host) {
        hash_data(req.http.host);
    } else {
        hash_data(server.ip);
    }
    # Add device type to cache key
    if (req.http.X-Device-Type) {
        hash_data(req.http.X-Device-Type);
    }
    return(lookup);
}

sub vcl_recv {
    # Detect device type by User-Agent
    if (req.http.User-Agent ~ "(?i)mobile|android|iphone|ipod|blackberry|opera mini|iemobile") {
        set req.http.X-Device-Type = "mobile";
    } else {
        set req.http.X-Device-Type = "desktop";
    }
}

Debugging and Metrics

sub vcl_deliver {
    # Add debug headers (remove in prod or restrict by IP)
    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
        set resp.http.X-Cache-Hits = obj.hits;
    } else {
        set resp.http.X-Cache = "MISS";
    }
    set resp.http.X-Served-By = server.hostname;
    # Remove internal headers
    unset resp.http.X-Powered-By;
    unset resp.http.Server;
    unset resp.http.X-Varnish;
    unset resp.http.Via;
}

Monitoring via varnishstat and varnishlog:

# Current hit rate
varnishstat -f MAIN.cache_hit,MAIN.cache_miss

# Live log filtered by URL
varnishlog -q 'ReqURL ~ "^/news/"' -g request

# Top cache miss by URL
varnishtop -i ReqURL -q 'VCL_call eq "MISS"'

Typical Project Timeline

Day 1–2—traffic audit, analyze backend response headers, identify non-cacheable patterns (cookies, Cache-Control: private).

Day 3–4—write basic VCL: URL normalization, strip marketing parameters, separate static and dynamic.

Day 5—configure grace, health checks, test on staging with varnishtest.

Day 6—implement invalidation via PURGE/xkey, integrate with CMS or deploy pipeline.

Day 7—load testing, configure hit rate monitoring, final deploy.

Complex cases (A/B testing via Varnish, ESI includes, multi-level caching with Nginx) add 3–5 days.