Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Httpcache Laravel Package

barryvdh/laravel-httpcache

HTTP response caching middleware for Laravel. Cache full responses with a simple middleware, tag and invalidate cached pages, and integrate Symfony's HTTP cache for faster load times with minimal code changes and no extra infrastructure.

View on GitHub
Deep Wiki
Context7

Getting Started

The install is two commands and a config publish:

composer require barryvdh/laravel-httpcache
php artisan vendor:publish --provider="Barryvdh\HttpCache\ServiceProvider"   # config/http_cache.php

The provider is auto-discovered, so there is no registration step beyond publishing the config. Do that publish first — the shipped default_ttl is 0, which means the middleware is a silent passthrough until you either raise that value or set Cache-Control explicitly on a response.

Read three things before you write any application code, in this order:

  1. config/http_cache.php — specifically options.default_ttl and the cache_control block (public, max_age, s_maxage, must_revalidate, private, no_cache). This file is your fleet-wide policy and explains most "it isn't caching" reports.
  2. Barryvdh\HttpCache\Middleware\CacheRequests — it is short. You need to understand that on a hit it short-circuits the entire inner middleware stack, because that single fact drives almost every gotcha below.
  3. Barryvdh\HttpCache\HttpCache — the Symfony HttpCache subclass that wires Laravel's cache store in as the Symfony Store. This is the file you subclass when you eventually need purge or instrumentation.

The mental model matters more than the API: this is not Cache::remember() for controllers. It is a tiny reverse proxy mounted inside your app that stores the full raw HTTP response — status, headers, body — when the request/response pair declares itself cacheable. If the response is not cacheable, the middleware does nothing at all.

Middleware registration has shifted across releases, so confirm against the README for your pinned version. Broadly, you either push Barryvdh\HttpCache\Middleware\CacheRequests onto the global stack (bootstrap/app.php on Laravel 11+, app/Http/Kernel.php before) or you apply the httpcache route middleware to a group. Strongly prefer the route middleware when your version offers it: it makes the cacheable surface explicit and greppable instead of accidental.

First use case — one read-heavy public GET endpoint, nothing else:

// routes/web.php
Route::middleware('httpcache')->group(function () {
    Route::get('/products', [ProductController::class, 'index']);
});

Then verify, twice, and compare:

curl -sI https://app.test/products | grep -iE 'cache-control|age'
curl -sI https://app.test/products | grep -iE 'cache-control|age'

The second call should show a Cache-Control you recognise and a non-zero Age, and it should not appear in your request log. If Age is absent, nothing was cached — go straight to the debugging checklist in Gotchas rather than guessing at config.

Implementation Patterns

Scope the middleware, don't globalise it. The dominant production failure with this package is enabling it globally and then maintaining an ever-growing ignore list. Invert the default: mount httpcache on route groups that are deliberately cacheable (/docs, /products, /api/v1/catalog) and keep auth, cart, checkout, and anything session-touching outside. Fewer config entries, far less to get wrong, and the cacheable surface stays reviewable in a diff.

Control freshness at three levels, in increasing specificity. Global default TTL in options.default_ttl; global header shaping via the cache_control block, which is how you set public, s-maxage, and must_revalidate without touching a single controller; and per-response headers where a route genuinely needs bespoke freshness:

return response()
    ->view('products.index', $data)
    ->header('Cache-Control', 'public, max-age=120, s-maxage=600');

Explicit response headers override the config-derived ones, which is the correct precedence — reserve this for the handful of routes that need it and let config carry everything else.

Conditional requests are the highest-value pattern and cost you almost nothing. If you set ETag or Last-Modified, Symfony's HttpCache handles If-None-Match and If-Modified-Since and returns 304 without invoking your controller at all. For JSON APIs this is the sweet spot: clients revalidate cheaply, you skip rendering entirely, and correctness stays high because revalidation is keyed on a validator you computed from the payload.

$etag = md5($payload.$updatedAt);
return response()->json($payload)->setEtag($etag);

Decide your Vary header before you enable caching, not after. Symfony keys entries on the URI plus the values of any headers listed in Vary. Localisation is the classic trap — adding Vary: Accept-Language multiplies your key space by the number of languages you serve, for every URL. Where you can, push the variant into the path (/en/docs/...) instead. It is cache-friendly, CDN-friendly, and trivially debuggable with curl. Only ever list headers you actually branch on.

Layer with a CDN deliberately. If a CDN sits in front, set s-maxage for the edge and max-age for the in-app copy, and decide which layer owns invalidation before either goes live. Two cache layers with independent TTLs and no agreed invalidation owner is a multi-day debugging session you can simply avoid by writing it down first.

Invalidation is coarse — design around it. There is no tag or purge API. The practical options, best to worst:

  • Version the key. Put a token in the path or query (/products?v=42). Deploy or content change bumps the version; old entries age out on TTL. Works across all nodes with zero coordination and no new infrastructure.
  • Namespace the store. Run cached responses on a dedicated cache store so php artisan cache:clear --store=httpcache is a blunt but safe purge that cannot nuke sessions, queues, or rate-limit counters.
  • Extend. Subclass Barryvdh\HttpCache\HttpCache or bind your own instance in the container to add a purge endpoint or an event listener that removes keys — only if you genuinely need targeted invalidation.

Make hits visible from day one. You cannot tune what you cannot measure. Add a small middleware inside the cache layer that appends an X-Cache-Status header, or simply log the Age header — a present, non-zero Age is the cheapest reliable hit indicator and it works in every environment without extra tooling.

Keep environments apart. Leave default_ttl at 0 in local and testing so developers never chase phantom stale state, and use a distinct cache store per environment so a staging warm-up or a load test can never poison production entries.

Gotchas and Tips

default_ttl = 0 means off, not forever. An enormous share of "the package does nothing" reports are simply the shipped default. Either raise it or send an explicit Cache-Control.

Middleware position decides what is actually cached. A hit short-circuits the entire inner stack, which is the whole point — but it also means anything those inner middleware would have injected (cookies, CORS, security headers, locale, debug toolbar) is frozen at the value from the original render. Order the stack so all response-shaping middleware runs inside the cache middleware, then verify by diffing a hit response against a miss response header by header. This is the single most common source of baffling production-only bugs.

Never cache a response that sets cookies or depends on the session. A cached Set-Cookie replays the same session cookie to every visitor — that is an auth incident, not a caching bug. Audit any route you place behind the cache with curl -sI and check for Set-Cookie. Same discipline applies to Authorization: do not lean on Symfony's private-response heuristics as your security boundary.

Per-user secrets inside rendered HTML get frozen. Any page with @csrf, a personalised greeting, or a relative timestamp (now()-derived "2 minutes ago") is served verbatim until expiry. Either keep those pages out of scope or render those fragments client-side.

Time-dependent output is the silent staleness source. Prices, stock counts, "last updated" badges — all frozen at cache time. Note that Date on a cached response is the original response date, not the current time; do not use it to reason about freshness. For each endpoint, write down an acceptable staleness window in seconds before enabling, and derive the TTL from it.

The file cache driver will quietly eat your disk. Entries are whole HTML or JSON documents with headers attached. Use Redis or memcached in production, set a sensible maxmemory-policy on Redis, and watch key count — your Vary combinations are the multiplier.

Clearing the wrong cache is a classic half-hour. php artisan cache:clear only helps if the HTTP cache lives in the default store. Check which store the package is configured to use and clear that. And in production with config:cache active, config edits do nothing until you re-run config:cache and restart your workers.

Proxy and scheme confusion corrupts your keys. The cache key includes host and scheme. Behind a load balancer without trusted-proxy configuration you end up with separate entries for http and https, or for differing Host values — same content, doubled storage, unpredictable hit rate. Configure TrustProxies ahead of the cache middleware.

Debugging checklist, in order:

  1. curl -sI twice and diff the headers. No Age on the second request means it was not cached — stop and investigate that before anything else.
  2. Is the status code in the cacheable set? 4xx and 5xx are not cached, and stale_if_error only helps once something is already stored.
  3. Is the response Cache-Control: private or no-cache? Laravel sets private on more responses than you expect; the package's cache_control block or an explicit header has to override it.
  4. Did a Vary header differ between the two requests? A shell curl and a browser rarely send the same Accept-Language — this one catches people constantly.
  5. Enable the HttpCache debug flag, or emit your own marker header, so the cache decision is reported rather than inferred.

Extension points worth memorising: the middleware class (headers, purge endpoints), the HttpCache subclass (store wiring, debug flag), the Symfony Store interface (custom storage semantics), and the cache_control config block (fleet-wide header policy). Everything else is Symfony behaviour in symfony/http-kernel's HttpCache — which you will end up reading, because the package README covers the happy path and not a great deal beyond it.

Pin the version. This is a single-maintainer package whose release cadence tracks Laravel majors. Pin ^1.0, upgrade deliberately alongside each Laravel bump, and keep in mind that falling back to raw Symfony HttpCache inside your own service provider is roughly a fifty-line escape hatch if maintenance ever lapses.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route