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.
Sledgehammer consists of two main packages:
libvmod-sledge
- The Varnish module required to run Sledge applicationssledge-tools
- Development tools including the vwasm
CLI for creating and building Sledge applicationsSledgehammer packages are distributed for Debian bookworm and the two most recent LTS releases of Ubuntu and AlmaLinux.
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
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.
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.
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:
The four callbacks are:
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.
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.
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.
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);
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
.
The Sledgehammer JavaScript API provides a web-standard compatible interface:
recv
:
event.request
continues the normal request processing.Response
object delivers a synth response of that object.event.requestAction()
transitions to the specified action.undefined
or anything else fails the client task.backend_request
:
event.request
continues the normal request processing.undefined
or anything else fails the backend task.backend_response
:
event.response
continues the normal response delivery.undefined
or anything else fails the backend task.deliver
:
event.response
continues the normal response delivery.undefined
or anything else fails the delivery task.request.headers
(read/write): Headers object for the requestrequest.url
(read/write): Get or set the request URLrequest.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 stringrequest.protocol
(read-only): Protocol used in the request.request.client.ip
(read-only): The client IP addressrequest.server.ip
(read-only): The local socket IP on which the request was received.response.headers
(read/write): Headers object for the responseresponse.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 contentresponse.ttl
: Set time-to-live for cachingresponse.grace
: Set grace period for cachingresponse.keep
: Set keep time for cachingresponse.set_body_callback(callback)
: Register a body transformation functionheaders.get(name)
: Get a header valueheaders.set(name, value)
: Set a header valueheaders.has(name)
: Check if a header existsevent.request
: Access to the Request objectevent.response
: Access to the Response object (only in backend_response
and deliver
)event.requestAction(request, options)
: Override default cache behavior, only available in recv
action
: “pass” or “pipe”backend
: Backend to use for the requestkvstore
: Access to a local (per application) instance of kv storekvstore.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 ttl
seconds (default to infinity).i64_get(key)
: Retrieve the i64 value stored under key
.console.log()
for debugging - output appears in varnishlogIf 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);
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();
}
This feature is available in Varnish Enterprise. Contact Varnish Software for licensing information.