Product Decisions This Supports
- Feature: speed up pages and jobs that fan out to third-party APIs. Any Laravel surface (dashboard widgets, product/price lookups, address or currency enrichment, webhook-triggered syncs, report generation) that makes synchronous Guzzle calls inherits the slowest dependency's latency. A handler-stack middleware makes repeat calls effectively free — a performance win with no user-facing behavior change.
- Roadmap: caching as an opt-in capability, not a rewrite. Because it plugs in at the HTTP client layer, caching can be introduced behind a feature flag per integration (one client, one host) without re-architecting service classes. This is a low-cost way to buy time on a performance roadmap item that would otherwise compete with product features.
- Build vs. buy: strongly favors buy. Hand-rolling RFC 7234-aware caching (freshness, validators,
Age, Vary, revalidation, stale-while-revalidate semantics) is weeks of work plus ongoing edge-case maintenance — binary payload truncation, DST arithmetic, Authorization handling, corrupted entries, Guzzle sink support have all been real bugs fixed in this package's history. Cost here is a composer require and a config decision. Build only if requirements are genuinely exotic.
- Unit economics: cut paid API spend and quota pressure. Fewer outbound calls to metered APIs (maps, enrichment, LLM/third-party SaaS) directly reduces per-request billing and lowers the chance of hitting rate limits during traffic spikes. This is a decision finance and platform both care about.
- Resilience and graceful degradation. The "always return stale on rejection" behavior turns an upstream outage or timeout into a degraded-but-serving experience rather than a hard failure — a lever for reliability SLOs and incident severity reduction.
- Unblocking integrations that "don't support caching." The greedy strategy lets us define our own TTL for upstreams that send no cache headers, converting unusable vendor APIs into cacheable dependencies. That frequently changes a "not viable" integration into a shippable one.
- Multi-vendor client consolidation. Delegating strategies let a single shared HTTP client carry different policies per host (public/shared cache for one vendor, private per-user cache for another, no caching for a third) — supporting a platform-owned HTTP client rather than per-team bespoke clients.
- Operational decision on cache substrate. Choosing Laravel cache storage means we reuse the existing Redis/Memcached investment and its monitoring, eviction, and flush tooling instead of adding a new datastore to the estate.
When to Consider This Package
Consider adoption when most of the following hold:
- The stack is Laravel/PHP 8.2+ (v8 requires it) with Guzzle 6/7/8 in the dependency graph — Laravel's
Http facade sits on Guzzle, so the handler stack is reachable.
- You have read-heavy, repeat outbound GETs to third-party APIs where responses are stable for seconds-to-hours: reference data, catalog/PIM lookups, geocoding, FX rates, profile enrichment.
- Upstream latency or per-call cost/quota is a measurable pain, and you can quantify calls per page view or per job.
- You already run a shared cache backend (Redis/Memcached via Laravel cache) and are comfortable operating it.
- Responses are safe to reuse across users (public/shared strategy) or you can correctly scope keys per user/tenant (
Authorization-aware keying, private strategy). This must be an explicit design review, not an assumption.
- You need RFC-compliant behavior — validators,
Cache-Control, Vary, revalidation — rather than a naive TTL key-value wrapper.
- Team accepts single-maintainer, MIT-licensed OSS with periodic major-version churn (v8 dropped PHP < 8.2 and moved to a PSR-20 clock); pinning and a scheduled upgrade cadence are in place.
Look elsewhere (or don't cache at the HTTP layer) when:
- You need cache invalidation triggered by your own writes. HTTP caching keys off URLs and headers; if correctness depends on "this record changed, purge it," you want application-level caching with explicit invalidation, an event-driven key scheme, or a CDN with purge APIs.
- The data is per-user, sensitive, or authorization-scoped and you cannot guarantee key isolation. A misconfigured public cache here is a data-leak incident, not a bug.
- Responses are non-idempotent or side-effecting (POST-based APIs, GraphQL mutations) or must never be replayed.
- Traffic is low-volume or uncached-first-hit dominant — cache hit rate won't justify the added failure mode; measure before adopting.
- You need cross-service or edge caching — an HTTP/CDN layer or reverse proxy is the better tool; a per-process handler stack won't help other consumers.
- The upstream already mandates short TTLs and you need real-time data (inventory, pricing, availability with strict freshness) — caching introduces staleness that product must accept in writing.
- You want a batteries-included, vendor-supported component with an SLA; here you're depending on community maintenance and should plan for the fork-ability of a small MIT package.
How to Pitch It (Stakeholders)
To executives (one paragraph): We can cut latency and third-party API spend on our most integration-heavy pages with a small, well-established open-source library — no new infrastructure and no user-visible change. It adds standards-compliant HTTP caching to the client we already use, reuses our existing Redis/Memcached cache, and also makes us more resilient: when a vendor is slow or down, we serve the last good response instead of an error. Cost is a dependency addition and a per-integration configuration decision, plus a policy review to ensure we never cache user-specific data in a shared cache. We propose a bounded pilot on one or two read-heavy integrations, measure hit rate, p95 latency, and vendor call volume, then decide on wider rollout.
To engineering: This is a Guzzle handler-stack middleware — push it on the client, pick a strategy (private, public/shared, greedy for header-less upstreams, delegating per host) and a storage adapter (native Laravel cache storage, so it rides our existing Redis). It's RFC 7234/9111-aware, PSR-7/PSR-6/PSR-16 compatible, and the recent 8.x line modernizes to PHP 8.2+, adds Guzzle 8 support, and fixes real correctness issues (DST freshness math, corrupted-entry invalidation, Authorization handling in shared caches, sink on cache hits, binary truncation). Guardrails: pin the version, add contract tests around cache-key composition and Vary/Authorization behavior, instrument hit/miss and stale-serve metrics, and make the strategy per-host explicit rather than a global default. The main risk is correctness of key scoping, not code quality — treat the cache key as part of the API contract.
To finance/ops: Every cache hit is a call we don't pay for and a request that doesn't consume a rate-limit token. Expect the biggest wins on high-fan-out pages and background syncs; we'll report measured call-volume reduction after the pilot rather than forecast it.