- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Caching Explained: From the Browser Cache to Redis, Layer by Layer
About Post
The fastest database query is the one you never run. The fastest API call is the one that never leaves the phone.
That's the whole promise of caching, and it's why "just cache it" is the first suggestion in every performance discussion. It's also why caching causes some of the strangest bugs you'll ever chase: the price that won't update, the user who sees someone else's dashboard, the server that falls over the exact second a cache entry expires.
The way to make sense of it is to stop thinking of "the cache" as one thing. A single request can pass through four or five caches before it ever reaches your code. Let's follow one, from the outside in.
Layer 1: the browser cache
The closest cache to the user is their own browser. If it already has a fresh copy of a file, it doesn't ask the network at all. Your server controls this with response headers, mainly Cache-Control:
# app.3f9a1c.js: content-hashed file name, never changes
Cache-Control: public, max-age=31536000, immutable
# /account: personal, always check with the server first
Cache-Control: private, no-cache
# /api/payment-card: never store at all
Cache-Control: no-store
The pattern for static assets is simple and powerful: put a hash of the content in the file name (Vite does this for you), and cache it for a year. When the file changes, the name changes, so the browser fetches the new one. No invalidation needed.
The naming trap: no-cache does not mean "don't cache". It means "you may store it, but check with me before using it". The browser sends the copy's ETag or last-modified date, and if nothing changed the server answers 304 Not Modified with no body. Cheap and always correct. no-store is the one that means "don't keep this anywhere".
Layer 2: the CDN
Next, a content delivery network (CloudFront, Cloudflare and friends) keeps copies on servers close to your users. A user far from your data centre gets your images and scripts from a nearby edge instead of a round trip to your origin.
CDNs follow the same headers, with one important extra: public versus private. public means shared caches like a CDN may store it. private means only the user's own browser may. You can also give the CDN a different lifetime with s-maxage.
This is the layer where caching mistakes become security incidents. If a page with personal data is served with public caching, the CDN can hand one user's page to the next user who asks for the same URL. Anything behind a login should be private or no-store, and be careful with CDN rules that "cache everything".
Layer 3: the application cache
When the request finally reaches your app, you can still avoid the expensive work: a heavy query, a slow external API, a report that aggregates thousands of rows. This is where Redis (or Memcached, or the database or file store for smaller apps) comes in, and in Laravel it looks like this:
$stats = Cache::remember(
"dashboard:stats:{$user->id}",
now()->addMinutes(10),
fn () => $this->buildDashboardStats($user),
);
remember() returns the cached value if it exists. If not, it runs the closure, stores the result for ten minutes and returns it. Three details matter more than they look:
- The key must include everything the value depends on. Forget the user ID in that key and every user sees the first user's dashboard. Include the locale, the filters, the page number, whatever changes the result.
- Cache data, not objects with baggage. Arrays and simple values serialise cleanly. Caching whole models with loaded relationships stores far more than you need and goes stale in surprising ways.
- Redis is shared. Unlike the browser cache, every server and every queue worker sees the same entries. That's what makes it useful, and what makes key design important.
Layer 4: the database's own caching
Developers often assume the database caches query results. MySQL used to have a query cache, but it was removed in MySQL 8.0 because it scaled badly under write-heavy workloads. Eloquent doesn't cache queries either.
What the database does cache is data pages. InnoDB's buffer pool keeps frequently used table and index pages in memory, which is why the second run of a query is often faster than the first. That's a good reason to give your database server enough memory, but it's not a substitute for indexes or an application cache.
Rule of thumb: cache as close to the user as you safely can. Static files belong in the browser and CDN, shared expensive results in Redis, and personal data should only ever be cached where only that person can see it.
The hard part, problem 1: invalidation
There's a famous line that the two hard things in computer science are cache invalidation and naming things. The reason is simple: the moment you cache something, you have two copies of the truth, and they will disagree.
You have three main tools:
- Expiry (TTL). Accept that data can be stale for up to N minutes. Simple and robust. Ideal for dashboards, counts and anything where "a few minutes old" is fine.
- Explicit forgetting. When the data changes, delete the cached copy:
Precise, but you have to remember every place that affects the value. Bulk updates that skip model events will skip this too.// In the Payment model protected static function booted(): void { static::saved(fn (Payment $payment) => Cache::forget("tenant:{$payment->tenant_id}:balance")); } - Versioned keys. Put a version or a timestamp in the key (
"unit:{$id}:v{$unit->updated_at->timestamp}"). When the data changes, the key changes, and the old entry simply expires unused.
My preference: use a TTL on everything, even entries you invalidate explicitly. It's the safety net for the invalidation you forgot.
The hard part, problem 2: the cache stampede
Picture a popular report cached for an hour. At the moment it expires, a hundred requests arrive. All of them miss the cache, all of them run the heavy query at the same time, and your database, which the cache was protecting, takes the full load at once. That's a cache stampede (also called a thundering herd).
Two good defences in Laravel:
// Fresh for 5 minutes; for up to 15, serve the old value
// while it's refreshed in the background after the response.
$report = Cache::flexible('reports:monthly', [300, 900], fn () => Report::monthly());
Cache::flexible() implements "stale-while-revalidate": users keep getting the slightly old value while one refresh happens behind the scenes, so nobody waits and the database isn't hit by everyone at once. The other option is an atomic lock with Cache::lock(), so only one process rebuilds the value while the others wait briefly or use what's there. Adding a little random jitter to TTLs also stops many keys from expiring at the same moment.
A quick decision list
- Static files with hashed names: cache for a year,
immutable. - HTML for logged-in users:
private, no-cache, orno-storefor sensitive pages. - Expensive shared results:
Cache::remember()with a sensible TTL. - Hot keys that are slow to rebuild:
Cache::flexible()or a lock. - Data that must always be exact (balances, permissions at the moment of a payment): read from the source.
Laravel's cache documentation covers the drivers, tags and locks in detail, and MDN's Cache-Control reference is the best guide to the headers.
What's the strangest stale-cache bug you've had to track down? Bonus points if it turned out to be a layer you didn't know was caching at all.

Be first to comment it...