Today, we’re releasing Ferron 3.0.0-beta.4, the fourth beta release of Ferron 3.
This is a correctness and reliability release. There are no breaking configuration changes. The two headline features — shared cache zones and rate limit zones — arrived alongside a large body of fixes that significantly improve protocol compliance and close several real-world reliability hazards that emerged from intensive testing of beta.3.
The shortest version: if you’re running beta.3 with HTTP caching enabled, this update matters.
Shared zones for cache and rate limiting
Beta.4 introduces a unified zone model for both the HTTP cache and rate limiting. In both cases, the idea is the same: named zones let multiple hostnames share a single backing store, with control over capacity, with metrics that identify which zone a measurement belongs to.
Cache zones
Previously, cache capacity was incorrectly configured per host, which created awkward duplication for multi-vhost setups and led to the regression described below. Beta.4 introduces explicit zone support in the cache directive.
Named zones can be pre-configured at global scope with a specific max_entries capacity. Host blocks then join a zone by name. When no zones are explicitly configured, all hosts continue sharing a single global zone by default, as they did before beta.3. Hosts that need isolation can opt out by specifying their own explicit zone directive.
{
cache {
max_entries 4096
zone "shared_assets" {
max_entries 8192
}
}
}
example.com {
cache {
zone "shared_assets"
}
}
www.example.com {
cache {
zone "shared_assets"
}
}A related fix: max_entries is now only evaluated on configuration reload rather than on every request. Previously, if different host blocks specified different capacities for the same cache store, the value could fluctuate at runtime and trigger premature LRU eviction. With this fix, capacity is stable between reloads.
All cache metrics — ferron.cache.requests, ferron.cache.entries, ferron.cache.stores, ferron.cache.evictions, ferron.cache.purges — now include a ferron.cache.zone attribute, so you can distinguish cache behavior per zone in dashboards without needing separate metric series per host.
Rate limit zones
Rate limiting gains the same model. The rate_limit directive now supports a zone subdirective, allowing multiple hostnames to share token bucket registries. Named zones are defined at global scope; hosts join them by name. Hosts without an explicit zone share a global zone by default.
Rate limit metrics (ferron.ratelimit.rejected, ferron.ratelimit.allowed) and structured log events now include a ferron.ratelimit.zone attribute.
One additional correctness improvement: the rate limit fingerprint now includes the key extractor type (ip, uri, header). Previously, rules with different key types could unintentionally share the same bucket registry, meaning an IP-based rule and a header-based rule on the same host could interfere with each other. They now maintain separate registries.
HTTP cache: a correctness overhaul
Beta.3 shipped stale-while-revalidate, stale-if-error, conditional revalidation, and purge propagation. Beta.4 fixes what turned out to be wrong with all of them — plus a few things that were wrong before. This section has a lot to cover.
Cache scope regression fixed
In beta.3, an unintended change caused caches to be scoped per host rather than globally, even when the max_entries configuration was defined at global scope. This is now corrected. If you were running beta.3 with multiple vhosts and expecting them to share a cache, they were actually maintaining separate stores. Beta.4 restores the pre-beta.3 global default, and the new zone directive (above) gives you explicit control when you do want per-host isolation.
Cache key security fix
A cache key construction bug allowed a crafted cookie value containing &cookie: to collide with a different user’s cache key. The consequence is cache poisoning: a request carrying a malicious cookie value could retrieve or populate a cache entry intended for a different request context.
This has been fixed by using a null byte (\0) as the separator in cache key construction rather than &, which is a valid character in cookie values. If you’re serving content with cookie-based cache variation — particularly for authenticated or personalized responses — updating to beta.4 is important.
Stale-while-revalidate inflight hang fixed
When a stale response triggered a background revalidation request, the inflight request tracking could cause the handling goroutine to hang. This has been fixed. Under sustained traffic to a stale cache entry, this could manifest as connections stacking up behind a revalidation that never completed.
304 revalidation now updates TTL
When a cached response was revalidated and the upstream returned 304 Not Modified, only the ETag and Last-Modified validators were updated in the stored entry. The TTL, stale-while-revalidate, stale-if-error, and must-revalidate directives from the 304 response’s Cache-Control headers were ignored. Per RFC 9111 §4.3.4, these should be applied from the revalidation response. This is now corrected.
The practical consequence of the old behavior: if an upstream sent a 304 with a shorter max-age than the original response, the cached entry would continue serving with the original (longer) TTL.
Status code preserved on revalidation
When a cached response with a non-200 status code (such as a 301 or 404) was revalidated and served, the response was incorrectly returned with a 200 OK status. The status code from the cached entry is now preserved correctly.
stale-if-error and must-revalidate interaction fixed
A stale response was incorrectly served under stale-if-error even when must-revalidate was present on the cached entry. Per RFC 9111 §4.3.4, must-revalidate prohibits stale serving unconditionally. This is now enforced.
Expires in the past edge case fixed
A response with an Expires header set to a past date and no Cache-Control directives was being cached for 5 minutes. This has been corrected: a past Expires with no overriding Cache-Control means the response expires immediately and is not cached.
Abuse protection responses no longer cached
403 Forbidden responses generated by the http-abuseban module were being passed to the HTTP cache layer and stored. Since these responses are generated before cache lookup, storing them was incorrect. They are now excluded from caching.
Cache variant metadata leak fixed
The internal variants_by_base map used for cache variant lookups was growing without bound, retaining records for all URLs ever cached even after those entries were purged. This map is now cleaned up when all entries for a base key are removed. Under heavy cache churn or aggressive purging, this could result in steady memory growth.
Static file serving: protocol compliance sweep
Static file serving went through a significant RFC 7232 and RFC 7233 compliance pass in beta.4. The fixes are numerous, so here’s a concise summary.
Range requests (RFC 7233)
Several issues with range request handling have been corrected:
If-Rangesupport added — theIf-Rangeheader (RFC 7233 §3.2) is now supported for conditional range requests. Clients receive a partial response when the entity tag or modification date matches, or a full200when the representation has changed.- Out-of-bounds ranges — a range request that exceeded the file size was returning
416 Range Not Satisfiable. It now correctly returns206 Partial Contentwith the available range, per RFC 7233 §2.1. - Multipart ranges — multipart range responses were missing the required
bytesprefix inContent-Rangepart headers, and an off-by-one error caused the final range’s data to be truncated. Both are fixed. - Invalid
Rangeheaders — syntactically invalidRangeheaders are now treated as absent (returning200 OK) rather than416, per RFC 7233 §3.1.
Conditional requests (RFC 7232)
If-None-Matchon POST — a POST request with a matchingIf-None-Matchheader was returning200 OK. It now correctly returns412 Precondition Failed, per RFC 7232 §4.2.If-Match: *on non-GET/HEAD —If-Match: *now correctly passes for POST and other non-GET/HEAD methods when a representation exists, per RFC 7232 §3.1.
Compression and metrics
- On-the-fly compression when precompressed files are unavailable — when a precompressed file existed for some, but not all, encodings, the server was not falling back to on-the-fly compression for the missing ones. This is now fixed.
- Missing
Content-Lengthfor compressed responses — compressed and precompressed responses now include the correctContent-Lengthheader. bytes_sentmetric — thebytes_sentmetric was reporting the original uncompressed file size for precompressed responses. It now reports the actual bytes sent over the wire.file_cache_controlpanic fix — a malformedfile_cache_controldirective value could previously panic the request handler. Invalid characters now produce a configuration validation warning instead.
Reverse proxy fixes
SRV record priority and weight
SRV upstream resolution was reading the priority and weight fields in the wrong order. Per RFC 2782, priority determines which group of backends to prefer, and weight controls distribution within a group. With the fields swapped, traffic could be routed to the wrong priority tier entirely. This is corrected.
WebSocket connection pool leak
HTTP 101 upgrade connections (WebSocket and similar) were not decrementing the connection pool’s outstanding counter when the upgraded connection closed. Each WebSocket connection permanently reduced the available pool capacity by one, eventually preventing new connections from being established. The pool slot is now released correctly on close.
Hop-by-hop header stripping
Outgoing proxy requests now strip hop-by-hop headers — Connection, Keep-Alive, Transfer-Encoding, TE, Trailer, Proxy-Authorization, Proxy-Authenticate — per RFC 7230 §6.1. Previously, clients could inject these headers to influence backend behavior. They are now removed before the upstream sees the request.
Looking toward stable
Beta.4 is a different kind of release from the ones before it. The feature list is short; the fix list is long. But the fixes matter: a cache that can poison entries, a stale revalidation that can hang under load, a SRV resolver that routes to the wrong priority tier — these are the kinds of issues that only become visible under real traffic.
Closing them out here, before stable, is exactly where this work belongs.
As always, feedback and bug reports are welcome.
Full changelog
The complete changelog for Ferron 3.0.0-beta.4 is available in the release notes.
Try it
- Documentation: https://ferron.sh/docs/v3
- GitHub repo: https://github.com/ferronweb/ferron/tree/develop-3.x
Install Ferron 3 using the installer:
sudo bash -c "$(curl -fsSL https://get.ferron.sh/v3)"