Search
Varnish Enterprise

Edge compute

Overview

Sledgehammer is a framework that allows you to write custom applications in your favorite programming language to handle and process Varnish requests and responses at the edge, by leveraging WebAssembly (WASM).

Sledgehammer enables you to run custom logic directly within Varnish, providing powerful edge computing capabilities.

This functionality is implemented through vmod_sledge and accompanying development tools that help you create Sledge applications from template projects.


sledge.png
Sledgehammer overview

Installation

Sledgehammer consists of two main packages:

  • libvmod-sledge - The Varnish module required to run Sledge applications
  • sledge-tools - Development tools including the vwasm CLI for creating and building Sledge applications

Sledgehammer packages are distributed for Debian bookworm and the two most recent LTS releases of Ubuntu and AlmaLinux.

Getting started with vwasm CLI

The vwasm tool is your primary interface for creating and managing Sledge applications. It handles project creation, building, and VCL generation. The vwasm utility is installed as part of the sledge-tools package.

To get started, ensure you have Node.js and npm installed (required for JavaScript/TypeScript development). You can install them by following the guide at https://nodejs.org/en/download/package-manager.

The vwasm tool provides the following commands:

vwasm <command> [arguments...]

Commands:
    help            Print usage information
    list-templates  List available project templates
    new            Create a new project
    validate       Validate environment dependencies
    generate-vcl   Generate VCL for loading a Sledge app
    run            Run a Sledge application
    build          Build the project

Creating a New sledge js application

To create a new JavaScript project:

vwasm new -t js-empty MyProject

This creates a project with the following structure:

MyProject/
├── .gitignore
├── package.json
├── README.md
├── rollup.config.mjs
├── sledge.project
└── src
    └── js
        └── index.js

The main part of your sledge application is the index.js file that contains the javascript code of your application.

The default generated program in the js-empty template contains a simple program that returns a response with a 200 status and a body containing an “It works!” heading.

Generating the VCL

After successfully creating your sledge project, you can now use the vwasm tool to generate a template vcl that will load your application and run it on every request. While being inside the sledge application directory, you can generate the VCL with the following command:

vwasm generate-vcl

This will output the VCL to stdout. If you want to write it to a file, you can add a -O <file> argument:

vwasm generate-vcl -O /path/to/output.vcl

The generated VCL contains the following parts:

vcl 4.1;

import sledge;

backend be1 {
    .host = "127.0.0.1";
    .port = "8081";
}

sub vcl_init {
     new app = sledge.app("/path/to/MyProject/target.tar");
}

It starts by importing the sledge vmod, then in vcl_init, it creates a sledge app object by calling the app() constructor with the path to your sledge application tar ball, which will load and initialise an instance of your wasm application.

sub vcl_recv {
     app.run();
}

sub vcl_synth {
     app.run();
     return( deliver );
}

sub vcl_backend_fetch {
     app.run();
}

sub vcl_backend_response {
     app.run();
}

sub vcl_deliver {
     app.run();
}

The rest of the vcl simply calls app.run(); from different VCL subroutines, which will internally invoke the callbacks defined in you sledge application and pass them the current request and/or response as a context.

Note that vcl_synth is a special case, as you do not define a callback for it in your sledge application code, but it is still necessary to call app.run() inside it so that synthetic responses that you return from recv can be delivered.

Let’s say that we only want to execute our app on requests targeting the /sledge URL to produce a synthetic backend response, while other requests continue to get regular VCL processing. The VCL can be updated to the following:

vcl 4.1;

import sledge;
import synthbackend;

sub vcl_init {
     # Load the slEdge Application
     new app = sledge.app("/tmp/bench-beresp/target/target.tar");
}

sub vcl_backend_fetch {
    if (bereq.url == "/sledge") {
        set bereq.backend = synthbackend.from_string(" ");
    }
}

sub vcl_backend_response {
    if (bereq.url == "/sledge") {
        app.run();
    }
}

# .. the rest of your VCL

vmod_synthbackend is used here to avoid doing an actual request to a real backend and transition to vcl_backend_response where sledge can produce a response body.

JavaScript Application Structure

A Sledge Application is a WebAssembly (WASM) module that integrates with Varnish to process HTTP requests and responses. Each application implements callback functions that will be invoked at various points in the request/response lifecycle. Each of these callbacks is passed a event object that contains the current request, and the response when it is available.

These callbacks give you fine-grained control over the request/response flow, allowing you to:

  • Modify requests/responses
  • Transform response bodies
  • Perform real-time data validation
  • Execute complex business logic at the edge
  • Integrate with external services
  • Implement custom caching strategies

The four callbacks are:

recv:

This is the first callback called, it is the equivalent of vcl_recv in VCL and is executed just after the request has been fully received. The event object passed to this method has a request attribute that gives you access to the current client request.

backend_request:

It is the equivalent of vcl_backend_fetch in VCL, it is called right before forwarding a request to the backend. The event object passed to this method has a request attribute that gives you access to the current backend request.

backend_response:

It is the equivalent of vcl_backend_response in VCL, it is called after receiving a complete response (headers) from the backend. The event object passed to this method has a request attribute that gives you access to the current backend request as well as a response attribute that contains the backend response.

deliver:

It is the equivalent of vcl_deliver in VCL, it is the last method called before delivering a response to a client. In this callback, the event object has a request attribute that gives you access to the client request as well as a response attribute that contains the response.

Since we want to create synthetic backend responses, we will only implement the backend_response callback. We set the response status to 200 and then we attach a response body callback that sets the body to <h1>It works!</h1>.

The sledge application code to do that is the following:

/**
 * Callback for the "backend_response" event.
 *
 * @param {Event} event
 * @returns
 */
function handleBeResponse(event) {
    let resp = event.response;
    resp.status = 200;
    resp.set_body_callback(input => {
         return ("<h1>It works!</h1>\n");
    });
    return resp;
  }

addEventListener("backend_response", handleBeResponse);

Building the project

To build the project, make sure you are inside the project directory, and run:

vwasm build

This will create a target folder containing the compiled WASM module.

You can now run varnish with the VCL configuration described earlier, and you should get the <h1>It works!</h1> response for requests targeting /sledge.

API Reference

The Sledgehammer JavaScript API provides a web-standard compatible interface:

Events

  • recv:
    • Returning event.request continues the normal request processing.
    • Returning a Response object delivers a synth response of that object.
    • Returning a event.requestAction() transitions to the specified action.
    • Returning undefined or anything else fails the client task.
  • backend_request:
    • Returning event.request continues the normal request processing.
    • Returning undefined or anything else fails the backend task.
  • backend_response:
    • Returning event.response continues the normal response delivery.
    • Returning undefined or anything else fails the backend task.
  • deliver :
    • Returning event.response continues the normal response delivery.
    • Returning undefined or anything else fails the delivery task.

Request Object

  • request.headers (read/write): Headers object for the request
  • request.url (read/write): Get or set the request URL
  • request.method (read/write): Get or set the HTTP method (GET, POST, etc.)
  • request.body (read-only, only in recv): Get the request body content as a string
  • request.protocol (read-only): Protocol used in the request.
  • request.client.ip (read-only): The client IP address
  • request.server.ip (read-only): The local socket IP on which the request was received.

Response Object

  • response.headers (read/write): Headers object for the response
  • response.status (read/write): Get or set the response status code (e.g., 200, 404)
  • response.status_text (read/write): Get or set the response status text (e.g., “OK”)
  • response.body (write): Set the response body content
  • response.ttl: Set time-to-live for caching
  • response.grace: Set grace period for caching
  • response.keep: Set keep time for caching
  • response.set_body_callback(callback): Register a body transformation function

Headers Object

  • headers.get(name): Get a header value
  • headers.set(name, value): Set a header value
  • headers.has(name): Check if a header exists

Event Object

  • event.request: Access to the Request object
  • event.response: Access to the Response object (only in backend_response and deliver)
  • event.requestAction(request, options): Override default cache behavior, only available in recv
    • Options:
      • action: “pass” or “pipe”
      • backend: Backend to use for the request

kvstore

  • kvstore : Access to a local (per application) instance of kv store
  • kvstore.global : Access to a kv store instance that is shared between applications (when enabled)
    • set(key, obj, ttl=-1) : Store the value obj under key for the duration ttl seconds (default to infinity).
    • get(key) : Retrieve the object stored under key.
    • remove(key) : Delete the object stored under key.
    • i64_set(key, val, ttl=-1) : Store the i64 value val under key for the duration ttl seconds (default to infinity).
    • i64_incr(key, val, ttl=-1) : Atomically add and fetch the i64 value val to the value stored under key for ttlseconds (default to infinity).
    • i64_get(key) : Retrieve the i64 value stored under key.

Debugging

  • Use console.log() for debugging - output appears in varnishlog
  • All errors are also logged to varnishlog

Example: Produce synthetic responses without caching

If your use case requires to produce a different synthetic response for every client request, then it is better to create the synthetic responses in recv for better performance. The following example shows how to do that.

VCL:

vcl 4.1;

sub vcl_init {
     # Load the slEdge Application
     new app = sledge.app("/path/to/MyProject/target.tar");
}

sub vcl_recv {
    if (req.url == "/sledge") {
        app.run();
    }
}

sub vcl_synth {
    if (req.url == "/sledge") {
        app.run();
        return( deliver );
    }
}

# .. the rest of your VCL

javascript:

function handleRecv(event) {
    let resp = new Response({status: 200, statusText: "OK"});
    resp.body = "<h1>It works!</h1>\n";
    return resp;
}

addEventListener("recv", handleRecv);

Example: JSON backend response manipulation

sledgehammer gives you the ability to manipulate backend response bodies by registering a body transformation callback using response.set_body_callback(callback). In the following example, we assume that the backend returns a JSON object for a certain API endpoint that has the following format:

{
        "id": 12345,
        "user": "bob",
        "status": "unsorted",
        "items": [120, 458, 9, 1056],
        "lastModified": "2025-09-23T12:54:19.510Z"
}

The object contains an array of integers items that might be unsorted, and we want to sort the array in our sledge application, and update the object status and lastModified date accordingly.

For this, we will be using the following sledge app:

function handleRecv(event) {
    let req = event.request;

    if (req.url == "/api_data.json") {
        return event.requestAction(event.request,
            {"action": "pass", "backend": "apibackend"}
        );
    }

    return event.request;
}

function handleBeResponse(event) {
    let resp = event.response;
    let req = event.request;

    if (req.url == "/api_data.json") {
        resp.set_body_callback(input => {
            const dataObject = JSON.parse(input);

            if (!dataObject.hasOwnProperty('status') || dataObject.status != "unsorted") {
                return input;
            }
            dataObject.items.sort((a, b) => a - b);
            dataObject.status = 'sorted';

            dataObject.lastModified = new Date().toISOString();
            return JSON.stringify(dataObject, null, 2);
        });
    }
    return resp;
}

addEventListener("recv", handleRecv);
addEventListener("backend_response", handleBeResponse);

In handleRecv, we make sure that all requests to /api_data.json are passed to the backend (no caching), and we also assign their backend to apibackend. This backend must of course be defined in the VCL.

Then, in handleBeResponse, we use resp.set_body_callback() to attach a callback function that will be executed on the response body. This callback gets the response body as a string argument, and is responsible to return a string.

The first thing we do is that we check that the json object we received has a status property, and that its value is unsorted, if that is not the case we simply return the original response untouched. We then sort the items array, update the status to sorted, and finally update the lastModified property with the current date.

We use the following VCL to load and run our sledge app:

vcl 4.1;

import sledge;

backend be1 {
    .host = "127.0.0.1";
    .port = "8081";
}

backend apibackend {
    .host = "127.0.0.1";
    .port = "8080";
}

sub vcl_init {
     # Load the slEdge Application
     new app = sledge.app("/tmp/test/target/target.tar");
}

sub vcl_recv {
     app.run();
}

sub vcl_backend_response {
     app.run();
}

Availability

This feature is available in Varnish Enterprise. Contact Varnish Software for licensing information.


®Varnish Software, Wallingatan 12, 111 60 Stockholm, Organization nr. 556805-6203