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

Guzzle Cache Middleware Laravel Package

kevinrob/guzzle-cache-middleware

A PSR-7 compatible HTTP cache middleware for Guzzle 6+ with RFC 7234 compliance. Pluggable storage backends include Laravel cache, Flysystem, PSR-6, PSR-16, and WordPress Object Cache. Add it to the HandlerStack to speed up repeated API calls.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit

  • The package is a Guzzle HandlerStack middleware, not a Laravel framework package. Its Laravel relevance comes from one built-in storage adapter (LaravelCacheStorage) wrapping Illuminate\Cache, which means it can reuse the exact Redis/Memcached/database cache store already provisioned for the application — no new infrastructure, no new credentials, no new operational surface. That is the single strongest argument for adoption from a cost/ownership standpoint.
  • Value is concentrated in outbound HTTP: third-party API reads (pricing, catalog, CRM lookups, geocoding, OAuth metadata/JWKS, feature-flag or config endpoints). Typical impact is p95 latency reduction on page renders and a reduction in vendor request volume (which is often directly metered/billed). It does nothing for inbound request caching — do not let stakeholders conflate it with response caching for your own API.
  • The abstraction layers are sensible: CacheMiddleware → strategy (PrivateCacheStrategy, PublicCacheStrategy, GreedyCacheStrategy, DelegatingCacheStrategy, NullCacheStrategy) → storage (LaravelCacheStorage, Psr6CacheStorage, Psr16, FlysystemStorage, WordPress). The delegating strategy is the important one architecturally: it lets you scope caching per host/route and default to NullCacheStrategy for anything not explicitly opted in. That fail-closed posture is what makes this deployable in a multi-tenant SaaS.
  • Correctness is delegated to HTTP semantics rather than invented policy — the stated goal is RFC 7234 compliance (superseded by RFC 9111, which recent releases have been converging toward, e.g. the Authorization-header fix in v7). This is good: it means cache behavior is driven by vendor Cache-Control/Expires headers, so upstream teams retain control and you avoid hardcoded TTL sprawl.

Integration feasibility

  • Low-to-moderate. Composer install, one $stack->push(...) in the client factory, one service-provider binding. No schema, no migrations, no queue, no daemon. In a Laravel app the friction point is that Illuminate\Http\Client builds its own Guzzle HandlerStack; injecting the middleware requires either passing a fully constructed handler option through Http::globalOptions([...]) or centralizing client construction in a small factory/singleton. Both are a day of work, not a sprint.
  • The Flysystem and WordPress adapters are irrelevant to a Laravel deployment; only LaravelCacheStorage and Psr6CacheStorage matter. Flysystem-on-local-disk is a trap in containerized environments — recommend Redis via LaravelCacheStorage and explicitly forbid filesystem storage in the ADR.
  • Ecosystem signal is thin: 437 stars and a reported 0 dependents means the reverse-dependency graph gives you no safety net and no reference implementations to copy. The Drupal guzzle_cache module is the only substantive third-party integration cited. Treat this as a single-maintainer library you are effectively adopting, not a de facto standard.

Technical risk

  • Maintainer concentration. Release history plus the README's "buy me a coffee" and Copilot-authored PRs indicate essentially one primary maintainer. MIT license means you can fork, and the code volume is small — but budget for the possibility that you own this in 24 months. Mitigation: pin minor versions, keep the middleware behind a thin internal wrapper class so a fork or replacement (e.g. Symfony HttpClient's cache contracts) is a contained swap.
  • PHP baseline jump. v8 drops PHP < 8.2 and adds Guzzle 8 and Laravel 13 support. If your fleet is on 8.1 or Guzzle 7, you are on v7 and outside the fix stream that just closed DST arithmetic, corrupted-entry invalidation, and the sink-option bug. That sink bug is material: before v8, downloads/streams written via a sink could not be honored on cache hits. If any workload writes to disk or streams large payloads, v8 or exclusion from caching is mandatory.
  • Semantic risk, not library risk. The failure modes that will actually page someone are policy failures: caching an authenticated response under a public strategy, omitting Authorization/tenant from the cache key, using GreedyCacheStrategy to override vendor headers on data with regulatory freshness constraints, or serving stale-on-rejection for money-moving values. The v5.1.0 "always return stale on rejection" behavior is a genuine availability win and a genuine correctness hazard depending on endpoint. This must be an allowlist decision, per endpoint, in writing.
  • No built-in observability. Expect no hit/miss counters, no cache-key tracing, no per-endpoint metrics. Instrumentation is your build: wrap the storage in a decorator or emit events around the client.
  • Score context. Composite 36.1 with opportunity 56.9 and maturity limited to README + releases signals a functional, narrowly-scoped utility with no formal documentation site, no published support policy, and no adoption breadth. Fund accordingly: it is a tactical optimization, not a platform commitment.

Key questions to resolve before approval

  1. Which named outbound endpoints are in scope, and what is each one's tolerance for staleness (seconds / minutes / never)?
  2. Does the vendor actually emit correct Cache-Control/Expires/Vary headers? Have we verified Vary handling for negotiated content (Accept-Language, locale, currency), or will we silently serve the wrong variant?
  3. For any GreedyCacheStrategy use, which headers must be folded into the cache key (Authorization, tenant ID, API version, locale)?
  4. Is PublicCacheStrategy ever acceptable given multi-tenancy, or is the whole deployment private-only?
  5. What is the invalidation story — do we need on-demand purge for vendor data corrected upstream, and via what mechanism?
  6. Which cache store backs this, and is its memory budgeted separately from session/queue caches?
  7. Do we require Guzzle 8 / PHP 8.2+ now, or are we shipping on v7 with known unfixed defects?
  8. Are there compliance constraints (GDPR, financial data retention, contractual no-caching clauses with the API vendor) that prohibit persisting responses at all?

Integration Approach

Stack fit

  • Assumes Laravel 9–13, PHP 8.1+ (8.2+ for v8), Guzzle 6/7 (v7) or 8 (v8), and an existing cache store. The LaravelCacheStorage adapter means it inherits whatever Cache::store() provides — TTL enforcement, eviction, tagging where supported, and your existing Redis monitoring. That is the path of least resistance and the path I would approve.
  • Do not use ArrayCachePool/in-process storage in production: per-request lifetime makes hit rates near zero under PHP-FPM and hides the cost/benefit from stakeholders.
  • Keep the middleware behind a single internal CachingHttpClientFactory (or a scoped Guzzle client bound in a service provider). No controller or job should ever construct a HandlerStack or reference Kevinrob\GuzzleCache\* directly. This is the one architectural non-negotiable: it preserves your options on a fork, a version pin, or a swap to a different caching mechanism.

Migration path

  • Greenfield adoption is trivial; the real work is retrofitting existing scattered Http::get(...) calls onto an injected client. Sequence: introduce the factory, migrate call sites without caching (behavior-preserving), then enable caching per endpoint.
  • If coming from Laravel's default Http facade with no caching, there is no data migration — the cache starts cold and warms naturally. No user-visible state change if strategies are correct.
  • If coming from an ad hoc Cache::remember() wrapper around HTTP calls: those wrappers must be removed in the same PR that enables the middleware, otherwise you get double caching with divergent TTLs and confusing invalidation. This is the most likely source of "we cached it but it's still stale" incidents.
  • Version migration: v6→v7 dropped doctrine/cache support — if any legacy code path used it, it moves to PSR-6/PSR-16. v7→v8 requires PHP 8.2, and is compatible with Laravel 13/Symfony 8. Plan v8 as the target; do not adopt v7 now unless blocked by the PHP runtime.

Compatibility

  • PSR-7/PSR-17 throughout, PSR-6 and PSR-16 storage adapters, PSR-20 clock (v8) which also gives you deterministic tests via a frozen clock. The clock injection is a real testability improvement — use it, and write regression tests for TTL boundary and DST transitions (the v8 fix exists precisely because that bug shipped).
  • Serialization compatibility matters if Redis is shared or if you upgrade across versions: v4.1.x fixed __serialize/unserialization regressions, v8 invalidates corrupted entries across all adapters. Always namespace the cache prefix by app version during a major upgrade to avoid reading old-serialized entries.
  • Confirm compatibility with your Guzzle middleware ordering. The cache middleware must sit at the top of the stack, above retry and above redirect handling, otherwise you cache post-retry results inconsistently. If you use guzzlehttp/retry-subscriber or Laravel's Http::retry(), define and test the intended interaction (recommendation: cache outside retry, so retries of a cache miss are not themselves cached as separate entries).

Sequencing

  1. ADR + endpoint inventory: classify every outbound dependency as cacheable / conditional / never.
  2. Build the client factory and wrap storage in a metrics decorator. Ship with NullCacheStrategy as the default — zero behavior change, full instrumentation.
  3. Enable PrivateCacheStrategy on one high-volume, low-risk, read-only GET endpoint. Measure hit ratio, p95 latency, upstream call count for two weeks.
  4. Expand to additional endpoints, each with an explicit strategy and key definition in the ADR. Introduce DelegatingCacheStrategy keyed by host once you exceed ~3 upstreams.
  5. Only if a vendor's headers are provably inadequate, evaluate GreedyCacheStrategy with explicit KeyValueHttpHeader for auth/tenant, as a separately reviewed change.
  6. Publish a runbook and a kill switch (env flag that swaps every strategy for NullCacheStrategy) before general rollout.

Operational Impact

Maintenance

  • Code maintenance is minimal — no runtime services, no migrations. Ongoing cost is (a) quarterly dependency review against Guzzle/Laravel major releases, (b) semantic review when a vendor changes its caching headers or auth scheme. The second is the recurring cost and it belongs to whoever owns the integration, not to platform.
  • Pin to a specific minor ("kevinrob/guzzle-cache-middleware": "~8.0") and treat major upgrades as planned work with a cache-prefix bump. Budget roughly 1–2 engineer-days per major version, more if the cache-key schema changes.
  • Bus factor is one. Document the fork procedure in the ADR — repository mirror, tag, and the wrapper interface that would need reimplementing. The wrapper makes this a contained exercise rather than a rewrite.

Support

  • No commercial support, no SLA, no vendor to escalate to. Issues are handled by the maintainer on a best-effort basis. You are the support tier for your own deployment; escalation path is "fork it."
  • Because the middleware only reads vendor headers, most "support" questions are really upstream-contract questions. Establish that triage boundary early so the platform team is not debugging a vendor's Cache-Control.

Scaling

  • Scales with your cache store, not with the library. Redis-backed LaravelCacheStorage handles high request volume fine, but two things will bite: memory (cached response bodies are unbounded in size unless you cap them) and key cardinality (per-tenant or per-user private caching multiplies keys by tenant count).
  • Mitigate by excluding large payloads (cap body size in the storage decorator), setting a store-level TTL ceiling, and using a dedicated Redis logical database or prefix so cache eviction under memory pressure cannot degrade sessions or queues.
  • No request coalescing. N concurrent requests for the same cold key will all miss and all hit the vendor — a thundering herd on expiry. If any upstream has tight rate limits, add a lock (Cache::lock) around the miss path for those specific endpoints, or stagger TTLs with jitter. This is the most likely scaling incident.
  • Horizontal scaling is a non-issue: the middleware is stateless, and a shared Redis store gives consistent behavior across pods. Local filesystem or array storage breaks this — another reason to forbid it.

Failure modes

Mode Symptom Blast radius Mitigation
Storage outage (Redis down) Every request is a miss; latency and vendor spend spike High, but degrades to no-cache, not to errors — if the adapter fails open Verify fail-open behavior in staging; alert on hit-ratio collapse
Storage failure closed 5xx on all cached endpoints Severe Test explicitly; wrap storage in a try/catch decorator that falls through to network
Wrong cache key (missing tenant/auth) Cross-tenant data exposure Critical Never use PublicCacheStrategy; explicit KeyValueHttpHeader for Authorization; adversarial test with two tenants
Stale-on-rejection serving obsolete data Wrong prices/inventory/entitlements shown Moderate–high Per-endpoint staleness budget; disable for money-moving reads
Corrupted serialized entry 500s or empty responses Moderate (fixed in v8) Stay on v8; version-prefix cache keys per release
Vendors stop sending cache headers Silent hit-rate drop to zero; benefit disappears quietly Low correctness, high cost Alert on per-endpoint hit ratio, not just aggregate
sink/stream mishandling (pre-v8) Truncated or empty downloaded files High for file workloads v8 minimum, or exclude streaming endpoints
TTL/DST arithmetic (pre-v8) Entries expiring an hour early/late at DST boundaries Low, hard to diagnose v8 minimum; regression test with injected PSR-20 clock

The dominant architectural property to preserve: this middleware must be able to fail without failing the request. Every mitigation above reduces to wrapping storage so that any exception is a cache miss.

Ramp-up

  • For a mid-level Laravel engineer familiar with HTTP semantics: half a day to first working integration, 2–3 days to a properly instrumented, allowlisted rollout. The API surface is small and the README examples are directly copy-pasteable.
  • The scarce skill is not the library — it is HTTP caching literacy: Cache-Control directives, Vary, Age, revalidation vs. freshness, and the difference between private and shared caches. Budget for a short internal primer and a written policy doc, and require that every enabled endpoint names its strategy and key in the ADR. Without that discipline, this package reliably produces "why is this user seeing someone else's data" incidents.
  • Onboarding artifact: one page listing each cached endpoint, its strategy, its cache key composition, its TTL/staleness tolerance, its owner, and the kill-switch procedure. That page, not the code, is what makes this operationally safe.
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.
dakiquang/laravel-oauth-refresh-lock
woda/laravel-worktrees
dalefranks/ramasite
faghy/laracashbook
roundly-consulting/google-places-for-laravel
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
adachsoft/changelog-linter