SkillAgentSearch skills...

Ledge

An RFC compliant and ESI capable HTTP cache for Nginx / OpenResty, backed by Redis

Install / Use

/learn @ledgetech/Ledge
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Ledge

Build Status

An RFC compliant and ESI capable HTTP cache for Nginx / OpenResty, backed by Redis.

Ledge can be utilised as a fast, robust and scalable alternative to Squid / Varnish etc, either installed standalone or integrated into an existing Nginx server or load balancer.

Moreover, it is particularly suited to applications where the origin is expensive or distant, making it desirable to serve from cache as optimistically as possible.

Table of Contents

Installation

OpenResty is a superset of Nginx, bundling LuaJIT and the lua-nginx-module as well as many other things. Whilst it is possible to build all of these things into Nginx yourself, we recommend using the latest OpenResty.

1. Download and install:

2. Install Ledge using LuaRocks:

luarocks install ledge

This will install the latest stable release, and all other Lua module dependencies, which if installing manually without LuaRocks are:

3. Review OpenResty documentation

If you are new to OpenResty, it's quite important to review the lua-nginx-module documentation on how to run Lua code in Nginx, as the environment is unusual. Specifcally, it's useful to understand the meaning of the different Nginx phase hooks such as init_by_lua and content_by_lua, as well as how the lua-nginx-module locates Lua modules with the lua_package_path directive.

Back to TOC

Philosophy and Nomenclature

The central module is called ledge, and provides factory methods for creating handler instances (for handling a request) and worker instances (for running background tasks). The ledge module is also where global configuration is managed.

A handler is short lived. It is typically created at the beginning of the Nginx content phase for a request, and when its run() method is called, takes responsibility for processing the current request and delivering a response. When run() has completed, HTTP status, headers and body will have been delivered to the client.

A worker is long lived, and there is one per Nginx worker process. It is created when Nginx starts a worker process, and dies when the Nginx worker dies. The worker pops queued background jobs and processes them.

An upstream is the only thing which must be manually configured, and points to another HTTP host where actual content lives. Typically one would use DNS to resolve client connections to the Nginx server running Ledge, and tell Ledge where to fetch from with the upstream configuration. As such, Ledge isn't designed to work as a forwarding proxy.

Redis is used for much more than cache storage. We rely heavily on its data structures to maintain cache metadata, as well as embedded Lua scripts for atomic task management and so on. By default, all cache body data and metadata will be stored in the same Redis instance. The location of cache metadata is global, set when Nginx starts up.

Cache body data is handled by the storage system, and as mentioned, by default shares the same Redis instance as the metadata. However, storage is abstracted via a driver system making it possible to store cache body data in a separate Redis instance, or a group of horizontally scalable Redis instances via a proxy, or to roll your own storage driver, for example targeting PostreSQL or even simply a filesystem. It's perhaps important to consider that by default all cache storage uses Redis, and as such is bound by system memory.

Back to TOC

Cache keys

A goal of any caching system is to safely maximise the HIT potential. That is, normalise factors which would split the cache wherever possible, in order to share as much cache as possible.

This is tricky to generalise, and so by default Ledge puts sane defaults from the request URI into the cache key, and provides a means for this to be customised by altering the cache_key_spec.

URI arguments are sorted alphabetically by default, so http://example.com?a=1&b=2 would hit the same cache entry as http://example.com?b=2&a=1.

Back to TOC

Streaming design

HTTP response sizes can be wildly different, sometimes tiny and sometimes huge, and it's not always possible to know the total size up front.

To guarantee predictable memory usage regardless of response sizes Ledge operates a streaming design, meaning it only ever operates on a single buffer per request at a time. This is equally true when fetching upstream to when reading from cache or serving to the client request.

It's also true (mostly) when processing ESI instructions, except for in the case where an instruction is found to span multiple buffers. In this case, we continue buffering until a complete instruction can be understood, up to a configurable limit.

This streaming design also improves latency, since we start serving the first buffer to the client request as soon as we're done with it, rather than fetching and saving an entire resource prior to serving. The buffer size can be tuned even on a per location basis.

Back to TOC

Collapsed forwarding

Ledge can attempt to collapse concurrent origin requests for known (previously) cacheable resources into a single upstream request. That is, if an upstream request for a resource is in progress, subsequent concurrent requests for the same resource will not bother the upstream, and instead wait for the first request to finish.

This is particularly useful to reduce upstream load if a spike of traffic occurs for expired and expensive content (since the chances of concurrent requests is higher on slower content).

Back to TOC

Advanced cache patterns

Beyond standard RFC compliant cache behaviours, Ledge has many features designed to maximise cache HIT rates and to reduce latency for requests. See the sections on Edge Side Includes, serving stale and revalidating on purge for more information.

Back to TOC

Minimal configuration

Assuming you have Redis running on localhost:6379, and your upstream is at localhost:8080, add the following to the nginx.conf file in your OpenResty installation.

http {
    if_modified_since Off;
    lua_check_client_abort On;

    init_by_lua_block {
        require("ledge").configure({
            redis_connector_params = {
                url = "redis://127.0.0.1:6379/0",
            },
        })

        require("ledge").set_handler_defaults({
            upstream_host = "127.0.0.1",
            upstream_port = 8080,
        })
    }

    init_worker_by_lua_block {
        require("ledge").create_worker():run()
    }

    server {
        server_name example.com;
        listen 80;

        location / {
            content_by_lua_block {
                require("ledge").create_handler():run()
            }
        }
    }
}

Back to TOC

Config systems

There are four different layers to the configuration system. Firstly there is the main Redis config and handler defaults config, which are global and must be set during the Nginx init phase.

Beyond this, you can specify handler instance config on an Nginx location block basis, and finally there are some performance tuning config options for the worker instances.

In addition, there is an events system for binding Lua functions to mid-request events, proving opportunities to dynamically alter configuration.

Back to TOC

Events system

Ledge makes most of i

View on GitHub
GitHub Stars459
CategoryDevelopment
Updated13d ago
Forks57

Languages

Lua

Security Score

85/100

Audited on Mar 17, 2026

No findings