<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged redis</title>
<link>https://coderlegion.com/tag/redis</link>
<description>Powered by Question2Answer</description>
<item>
<title>Building the MyZubster Metaverse: From Prototype to Secure Realtime Infrastructure</title>
<link>https://coderlegion.com/32147/building-the-myzubster-metaverse-from-prototype-to-secure-realtime-infrastructure</link>
<description>&lt;p&gt;Building the MyZubster Metaverse: From Prototype to Secure Realtime Infrastructure&lt;br&gt;
The MyZubster Metaverse is evolving from a simple interactive experience into a distributed platform designed around secure identity, realtime communication, reliability, privacy, and verifiable collaboration.&lt;br&gt;
During the latest development phase, we focused on the infrastructure required to operate the metaverse safely under real production conditions.&lt;br&gt;
This article explains what we implemented, how the architecture works, what we verified, and what remains before the system can be considered production-ready.&lt;br&gt;
The Current Architecture&lt;br&gt;
The metaverse now follows a layered architecture:&lt;br&gt;
Authenticated user&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MyZubster session&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Short-lived realtime token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Authorized realtime channel&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Distributed gateway&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis coordination layer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Presence, rate limits and observability&lt;br&gt;
The objective is to prevent clients from connecting directly to unrestricted realtime resources.&lt;br&gt;
Every connection must originate from an authenticated MyZubster session and receive only the permissions required for its specific room or operation.&lt;br&gt;
Authenticated Realtime Sessions&lt;br&gt;
We introduced a dedicated token exchange for realtime connections.&lt;br&gt;
The server:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;verifies the current MyZubster session;&lt;/li&gt;
&lt;li&gt;checks whether the user may access the requested resource;&lt;/li&gt;
&lt;li&gt;generates a short-lived realtime token;&lt;/li&gt;
&lt;li&gt;restricts that token to authorized channels;&lt;/li&gt;
&lt;li&gt;rejects anonymous or malformed requests.&lt;br&gt;
This limits the damage caused by leaked tokens and prevents clients from subscribing to arbitrary rooms.&lt;br&gt;
A client cannot simply invent a room identifier and connect to it. Authorization is evaluated server-side before realtime access is granted.&lt;br&gt;
Channel Authorization&lt;br&gt;
Realtime channels are treated as protected resources.&lt;br&gt;
The authorization layer validates:&lt;/li&gt;
&lt;li&gt;user identity;&lt;/li&gt;
&lt;li&gt;session validity;&lt;/li&gt;
&lt;li&gt;resource ownership;&lt;/li&gt;
&lt;li&gt;room membership;&lt;/li&gt;
&lt;li&gt;token expiration;&lt;/li&gt;
&lt;li&gt;permitted operations.&lt;br&gt;
This establishes a clear trust boundary between the browser and the realtime gateway.&lt;br&gt;
The client requests access, but the server remains responsible for deciding whether that access is legitimate.&lt;br&gt;
Distributed Presence with Redis&lt;br&gt;
In-memory presence works only while a single application instance is running. It becomes unreliable when multiple instances or serverless functions are involved.&lt;br&gt;
We therefore introduced Redis-backed presence coordination.&lt;br&gt;
Redis is used to track:&lt;/li&gt;
&lt;li&gt;active participants;&lt;/li&gt;
&lt;li&gt;room membership;&lt;/li&gt;
&lt;li&gt;temporary presence state;&lt;/li&gt;
&lt;li&gt;expiration of abandoned sessions;&lt;/li&gt;
&lt;li&gt;coordination between separate gateway instances.&lt;br&gt;
Presence entries have a limited lifetime. If a client disappears without completing a normal disconnection, the corresponding state can expire automatically instead of remaining indefinitely.&lt;br&gt;
Graceful Redis Failure Handling&lt;br&gt;
Redis is now considered an explicit infrastructure dependency.&lt;br&gt;
The system distinguishes between:&lt;/li&gt;
&lt;li&gt;the application process being alive;&lt;/li&gt;
&lt;li&gt;the realtime gateway being operational;&lt;/li&gt;
&lt;li&gt;Redis being reachable;&lt;/li&gt;
&lt;li&gt;the complete service being ready to accept traffic.&lt;br&gt;
Failures are handled deliberately rather than silently ignored. This is important because returning a successful health response while the coordination layer is unavailable would provide misleading information to deployment systems and operators.&lt;br&gt;
Distributed Rate Limiting&lt;br&gt;
Local counters cannot reliably enforce limits across multiple runtime instances.&lt;br&gt;
We implemented a distributed rate-limiting foundation backed by Redis so that requests are evaluated against shared state.&lt;br&gt;
The rate-limiting system is designed to protect:&lt;/li&gt;
&lt;li&gt;realtime token generation;&lt;/li&gt;
&lt;li&gt;connection attempts;&lt;/li&gt;
&lt;li&gt;room joins;&lt;/li&gt;
&lt;li&gt;message publication;&lt;/li&gt;
&lt;li&gt;presence updates;&lt;/li&gt;
&lt;li&gt;abuse-sensitive API routes.&lt;br&gt;
This provides a foundation for defending the metaverse against spam, accidental request loops and automated abuse.&lt;br&gt;
Future work will refine quotas by route, identity, IP risk and operation type.&lt;br&gt;
Privacy-Safe Observability&lt;br&gt;
Reliability requires visibility, but observability must not expose private user information.&lt;br&gt;
The realtime infrastructure now supports structured operational signals for:&lt;/li&gt;
&lt;li&gt;authorization failures;&lt;/li&gt;
&lt;li&gt;connection lifecycle events;&lt;/li&gt;
&lt;li&gt;Redis availability;&lt;/li&gt;
&lt;li&gt;rate-limit decisions;&lt;/li&gt;
&lt;li&gt;degraded states;&lt;/li&gt;
&lt;li&gt;health and readiness checks.&lt;br&gt;
The goal is to make infrastructure problems diagnosable without logging raw credentials, session tokens or unnecessary personal information.&lt;br&gt;
Health and Readiness Endpoints&lt;br&gt;
A process being alive does not mean it is ready to serve realtime traffic.&lt;br&gt;
We extended the health model to include Redis readiness. The service can now report a degraded state when Redis is unavailable instead of returning an unconditional success response.&lt;br&gt;
Conceptually:&lt;br&gt;
Process alive + Redis unavailable&lt;pre&gt;&lt;code&gt;       ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Service is not fully ready&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Health response reports degradation&lt;br&gt;
This distinction is essential for:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;deployment validation;&lt;/li&gt;
&lt;li&gt;load balancers;&lt;/li&gt;
&lt;li&gt;automated monitoring;&lt;/li&gt;
&lt;li&gt;incident diagnosis;&lt;/li&gt;
&lt;li&gt;rolling releases;&lt;/li&gt;
&lt;li&gt;recovery procedures.&lt;br&gt;
Shared Redis Client&lt;br&gt;
A reusable Redis client layer was introduced to avoid creating duplicated connections across services and requests.&lt;br&gt;
The shared client centralizes:&lt;/li&gt;
&lt;li&gt;configuration;&lt;/li&gt;
&lt;li&gt;connection reuse;&lt;/li&gt;
&lt;li&gt;error handling;&lt;/li&gt;
&lt;li&gt;readiness checks;&lt;/li&gt;
&lt;li&gt;shutdown behavior;&lt;/li&gt;
&lt;li&gt;instrumentation.&lt;br&gt;
Centralizing this behavior reduces connection pressure and gives every Redis-dependent feature a consistent operational model.&lt;br&gt;
Preview Infrastructure&lt;br&gt;
The new realtime changes were deployed to an isolated Vercel Preview environment.&lt;br&gt;
This allows us to validate infrastructure modifications before they affect the public production deployment.&lt;br&gt;
The preview environment provides a safe place to test:&lt;/li&gt;
&lt;li&gt;application builds;&lt;/li&gt;
&lt;li&gt;serverless deployment behavior;&lt;/li&gt;
&lt;li&gt;environment configuration;&lt;/li&gt;
&lt;li&gt;readiness responses;&lt;/li&gt;
&lt;li&gt;Redis connectivity;&lt;/li&gt;
&lt;li&gt;authentication flows;&lt;/li&gt;
&lt;li&gt;realtime regressions.&lt;br&gt;
Production remains unchanged while the current pull request is still under review.&lt;br&gt;
Testing and Continuous Integration&lt;br&gt;
The implementation is covered by automated backend tests, including readiness behavior and Redis degradation scenarios.&lt;br&gt;
The principal validation pipeline successfully completed:&lt;/li&gt;
&lt;li&gt;backend tests;&lt;/li&gt;
&lt;li&gt;frontend tests;&lt;/li&gt;
&lt;li&gt;linting;&lt;/li&gt;
&lt;li&gt;application builds;&lt;/li&gt;
&lt;li&gt;Seller Free policy verification;&lt;/li&gt;
&lt;li&gt;continuous evidence generation;&lt;/li&gt;
&lt;li&gt;preview deployment.&lt;br&gt;
This provides strong evidence that the new infrastructure integrates correctly with the existing application.&lt;br&gt;
However, passing tests alone do not make the service production-ready. Runtime drills, dependency remediation and real multi-instance load testing are still required.&lt;br&gt;
Current Development Status&lt;br&gt;
The realtime readiness work is currently available in a draft pull request:&lt;br&gt;
MyZubster realtime readiness PR #40&lt;br&gt;
The implementation includes four focused commits:&lt;br&gt;
feat(realtime): expose Redis readiness&lt;br&gt;
test(realtime): cover Redis readiness&lt;br&gt;
feat(realtime): gate health on Redis&lt;br&gt;
test(realtime): cover Redis health degradation&lt;br&gt;
Keeping the commits focused makes review and rollback easier.&lt;br&gt;
Current Security Blocker&lt;br&gt;
The security audit currently reports vulnerabilities in the transitive ip-address dependency for versions up to 10.7.3.&lt;br&gt;
The available patched version is 10.7.4.&lt;br&gt;
Before merging the realtime work, we need to:&lt;/li&gt;
&lt;li&gt;identify which direct dependency introduces ip-address;&lt;/li&gt;
&lt;li&gt;update the dependency chain safely;&lt;/li&gt;
&lt;li&gt;regenerate the lockfile;&lt;/li&gt;
&lt;li&gt;run the complete test suite;&lt;/li&gt;
&lt;li&gt;repeat the security audit;&lt;/li&gt;
&lt;li&gt;confirm that no high or critical vulnerability remains.&lt;br&gt;
The pull request should remain in draft until this has been addressed and the runtime verification has been completed.&lt;br&gt;
What Still Needs to Be Done&lt;/li&gt;
&lt;li&gt;Dependency remediation&lt;br&gt;
Upgrade the affected dependency chain and obtain a clean security audit.&lt;/li&gt;
&lt;li&gt;Direct runtime verification&lt;br&gt;
Verify the deployed preview response from:&lt;br&gt;
/api/realtime/health&lt;br&gt;
The response should explicitly show whether Redis is ready and whether the service is operating normally or in a degraded state.&lt;/li&gt;
&lt;li&gt;Redis failure drill&lt;br&gt;
Temporarily simulate Redis unavailability in an isolated environment and verify that:&lt;/li&gt;
&lt;li&gt;readiness fails correctly;&lt;/li&gt;
&lt;li&gt;the service does not claim to be healthy;&lt;/li&gt;
&lt;li&gt;errors remain controlled;&lt;/li&gt;
&lt;li&gt;recovery occurs after Redis reconnects.&lt;/li&gt;
&lt;li&gt;Multi-instance load testing&lt;br&gt;
Run concurrent tests against multiple gateway instances and verify:&lt;/li&gt;
&lt;li&gt;shared rate limits;&lt;/li&gt;
&lt;li&gt;consistent presence;&lt;/li&gt;
&lt;li&gt;room synchronization;&lt;/li&gt;
&lt;li&gt;connection cleanup;&lt;/li&gt;
&lt;li&gt;acceptable latency;&lt;/li&gt;
&lt;li&gt;stable Redis connection usage.&lt;/li&gt;
&lt;li&gt;Restart and recovery testing&lt;br&gt;
Restart application and Redis instances while clients are connected.&lt;br&gt;
We need evidence that the platform can recover without leaving permanent ghost sessions or inconsistent presence records.&lt;/li&gt;
&lt;li&gt;End-to-end acceptance testing&lt;br&gt;
The complete flow must be tested:&lt;br&gt;
Login&lt;br&gt;
→ request realtime token&lt;br&gt;
→ authorize channel&lt;br&gt;
→ connect&lt;br&gt;
→ join room&lt;br&gt;
→ publish event&lt;br&gt;
→ update presence&lt;br&gt;
→ disconnect&lt;br&gt;
→ clean up state&lt;br&gt;
Unauthorized variations must also be tested.&lt;/li&gt;
&lt;li&gt;Production rollout strategy&lt;br&gt;
The production release should be gradual:&lt;/li&gt;
&lt;li&gt;deploy to Preview;&lt;/li&gt;
&lt;li&gt;complete runtime verification;&lt;/li&gt;
&lt;li&gt;run load and failure drills;&lt;/li&gt;
&lt;li&gt;review monitoring signals;&lt;/li&gt;
&lt;li&gt;merge after approval;&lt;/li&gt;
&lt;li&gt;deploy production;&lt;/li&gt;
&lt;li&gt;observe error rates and Redis health;&lt;/li&gt;
&lt;li&gt;retain a documented rollback procedure.&lt;br&gt;
Longer-Term Metaverse Roadmap&lt;br&gt;
Once reliability and security are validated, development can move toward the experience layer.&lt;br&gt;
Planned areas include:&lt;/li&gt;
&lt;li&gt;WebXR support;&lt;/li&gt;
&lt;li&gt;immersive rooms;&lt;/li&gt;
&lt;li&gt;interactive avatars;&lt;/li&gt;
&lt;li&gt;spatial collaboration;&lt;/li&gt;
&lt;li&gt;verified professional profiles;&lt;/li&gt;
&lt;li&gt;Zorgax integration;&lt;/li&gt;
&lt;li&gt;verified knowledge inside virtual environments;&lt;/li&gt;
&lt;li&gt;community-managed spaces;&lt;/li&gt;
&lt;li&gt;portable identity;&lt;/li&gt;
&lt;li&gt;privacy-safe analytics;&lt;/li&gt;
&lt;li&gt;independent interoperable nodes.&lt;br&gt;
The long-term objective is not merely to build another virtual world.&lt;br&gt;
MyZubster aims to combine identity, knowledge, collaboration and transparent open-source development inside an interoperable environment.&lt;br&gt;
Lessons Learned&lt;br&gt;
The most important lesson from this phase is that realtime development is not simply about sending messages quickly.&lt;br&gt;
A reliable realtime platform must answer several harder questions:&lt;/li&gt;
&lt;li&gt;Who is allowed to connect?&lt;/li&gt;
&lt;li&gt;Which channels may they access?&lt;/li&gt;
&lt;li&gt;How is state shared across instances?&lt;/li&gt;
&lt;li&gt;What happens when Redis fails?&lt;/li&gt;
&lt;li&gt;How is abuse limited globally?&lt;/li&gt;
&lt;li&gt;Can operators distinguish “alive” from “ready”?&lt;/li&gt;
&lt;li&gt;Can the system recover cleanly after a restart?&lt;/li&gt;
&lt;li&gt;Can failures be investigated without exposing private data?&lt;br&gt;
These concerns are now becoming first-class parts of the MyZubster architecture.&lt;br&gt;
Conclusion&lt;br&gt;
The MyZubster Metaverse has progressed beyond an isolated prototype.&lt;br&gt;
We now have the foundation for:&lt;/li&gt;
&lt;li&gt;authenticated realtime access;&lt;/li&gt;
&lt;li&gt;server-controlled channel authorization;&lt;/li&gt;
&lt;li&gt;Redis-backed presence;&lt;/li&gt;
&lt;li&gt;distributed rate limiting;&lt;/li&gt;
&lt;li&gt;privacy-aware observability;&lt;/li&gt;
&lt;li&gt;dependency-aware health checks;&lt;/li&gt;
&lt;li&gt;isolated preview deployments;&lt;/li&gt;
&lt;li&gt;automated tests and evidence generation.&lt;br&gt;
The next milestone is to prove that this architecture remains reliable under dependency failures, restarts, concurrency and realistic traffic.&lt;br&gt;
Only after those checks—and after resolving the current security advisory—should the realtime infrastructure be promoted to production.&lt;br&gt;
MyZubster remains an open-source project, and technical review, testing and constructive contributions are welcome.&lt;br&gt;
Project Links&lt;/li&gt;
&lt;li&gt;MyZubster repository&lt;/li&gt;
&lt;li&gt;MyZubster Metaverse&lt;/li&gt;
&lt;li&gt;Realtime readiness pull request&lt;br&gt;
 Repository GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/danieldirimini-myzubster/myzubster&quot;&gt;https://github.com/danieldirimini-myzubster/myzubster&lt;/a&gt;&lt;br&gt;
Metaverso MyZubster: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/metaverse&quot;&gt;https://www.myzubster.com/metaverse&lt;/a&gt;&lt;br&gt;
Pull request Realtime Readiness #40: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/danieldirimini-myzubster/myzubster/pull/40&quot;&gt;https://github.com/danieldirimini-myzubster/myzubster/pull/40&lt;/a&gt;&lt;br&gt;
Roadmap Linear MYZ-83: &lt;a rel=&quot;nofollow&quot; href=&quot;https://linear.app/myzubster/issue/MYZ-83/b6-realtime-observability-reliability-and-load-tests&quot;&gt;https://linear.app/myzubster/issue/MYZ-83/b6-realtime-observability-reliability-and-load-tests&lt;/a&gt;&lt;br&gt;
Organizzazione GitHub MyZubster Ecosystem: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MyZubster-Ecosystem&quot;&gt;https://github.com/MyZubster-Ecosystem&lt;/a&gt;&lt;br&gt;
Profilo GitHub MyZubster: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/myzubster&quot;&gt;https://github.com/myzubster&lt;/a&gt;&lt;br&gt;
Sito principale: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com&quot;&gt;https://www.myzubster.com&lt;/a&gt;&lt;br&gt;
Knowledge Explorer: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/knowledge&quot;&gt;https://www.myzubster.com/knowledge&lt;/a&gt;&lt;br&gt;
Profilo Zorgax: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.myzubster.com/zorgax-profile-onboarding&quot;&gt;https://www.myzubster.com/zorgax-profile-onboarding&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32147/building-the-myzubster-metaverse-from-prototype-to-secure-realtime-infrastructure</guid>
<pubDate>Fri, 23 Oct 2026 05:22:14 +0000</pubDate>
</item>
<item>
<title>⚡️ Caching Strategies for Web Apps: From Browser to Backend</title>
<link>https://coderlegion.com/30544/caching-strategies-for-web-apps-from-browser-to-backend</link>
<description>&lt;p&gt;Caching is one of the &lt;strong&gt;most powerful ways to improve application performance&lt;/strong&gt;—reducing load times, server costs, and user frustration. Yet many developers treat caching as a “black box” or skip it due to fear of cache-related bugs.&lt;/p&gt;
&lt;p&gt;In this blog, we will:&lt;br&gt;
✅ Demystify caching: what it is and why it matters&lt;br&gt;
✅ Explore &lt;strong&gt;frontend (browser) caching strategies&lt;/strong&gt;&lt;br&gt;
✅ Dive into &lt;strong&gt;backend and distributed caching&lt;/strong&gt; with Go examples&lt;br&gt;
✅ Cover &lt;strong&gt;Cache-Control headers, ETag, stale-while-revalidate&lt;/strong&gt;&lt;br&gt;
✅ Discuss &lt;strong&gt;common pitfalls and best practices&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;By the end, you’ll know how to confidently implement caching in your React frontend and Go backend to build &lt;strong&gt;scalable, fast, and efficient applications&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-what-is-caching-&quot;&gt;🚀 What is Caching?&lt;/h3&gt;
&lt;p&gt;Caching means &lt;strong&gt;storing data temporarily&lt;/strong&gt; so future requests can be served faster without recomputing or refetching the same data repeatedly.&lt;/p&gt;
&lt;p&gt;Think of it like &lt;strong&gt;preparing tea and storing it in a flask&lt;/strong&gt;—you don’t need to reboil water each time you want to drink.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-why-caching-matters&quot;&gt;📈 Why Caching Matters&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Speed:&lt;/strong&gt; Return data faster to users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduce Server Load:&lt;/strong&gt; Avoid redundant computation or DB queries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Improve Reliability:&lt;/strong&gt; Serve cached responses when backend services are down.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduce Costs:&lt;/strong&gt; Less compute = lower cloud bills.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-caching-in-the-frontend-browser-&quot;&gt;🧩 Caching in the Frontend (Browser)&lt;/h3&gt;
&lt;h4 id=&quot;1-browser-cache-http-caching-&quot;&gt;1️⃣ Browser Cache (HTTP Caching)&lt;/h4&gt;
&lt;p&gt;Browsers automatically cache assets (JS, CSS, images) and API responses based on HTTP headers.&lt;/p&gt;
&lt;h5 id=&quot;key-headers-&quot;&gt;Key Headers:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Cache-Control&lt;/code&gt;: Defines how long and under what conditions the browser can reuse the response.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ETag&lt;/code&gt;: Helps validate cached data; server sends a hash, and browser sends it back to check if the data changed.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Expires&lt;/code&gt;: Sets a hard expiration time for cache.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stale-while-revalidate&lt;/code&gt;: Allows serving stale content while revalidating in the background for a seamless UX.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Cache-Control: public, max-age=3600, stale-while-revalidate=600
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ &lt;strong&gt;Use Case:&lt;/strong&gt; Cache API GET responses for non-user-specific data (e.g., blog posts, product catalogs).&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-client-side-data-fetching-libraries&quot;&gt;2️⃣ Client-Side Data Fetching Libraries&lt;/h4&gt;
&lt;p&gt;Libraries like &lt;strong&gt;React Query, SWR&lt;/strong&gt; provide built-in caching for API calls.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cache API responses in memory.&lt;/li&gt;
&lt;li&gt;Background revalidation for fresh data.&lt;/li&gt;
&lt;li&gt;Avoids unnecessary re-fetching on component re-mounts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ Use for &lt;strong&gt;per-session data caching&lt;/strong&gt; in React apps.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-caching-in-the-backend&quot;&gt;🗂️ Caching in the Backend&lt;/h3&gt;
&lt;h4 id=&quot;1-in-memory-caching&quot;&gt;1️⃣ In-Memory Caching&lt;/h4&gt;
&lt;p&gt;Store frequently accessed data in memory for fast retrieval.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Examples:&lt;/strong&gt; Golang’s &lt;code&gt;sync.Map&lt;/code&gt;, &lt;code&gt;groupcache&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Fastest but limited by server RAM and is not shared across instances.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ Use for small, frequently requested data (e.g., config, auth tokens).&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-distributed-caching&quot;&gt;2️⃣ Distributed Caching&lt;/h4&gt;
&lt;p&gt;Tools like &lt;strong&gt;Redis and Memcached&lt;/strong&gt; provide a shared cache layer accessible across multiple servers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Centralized cache for load-balanced apps.&lt;/li&gt;
&lt;li&gt;Supports expiration policies.&lt;/li&gt;
&lt;li&gt;Can handle large datasets.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;-go-example-using-redis-for-caching&quot;&gt;🔹 Go Example: Using Redis for Caching&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1. Install Redis client:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;go get github.com/go-redis/redis/v8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Basic Usage:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;rdb := redis.NewClient(&amp;amp;redis.Options{
    Addr: &quot;localhost:6379&quot;,
})

ctx := context.Background()

// Set cache
rdb.Set(ctx, &quot;product_123&quot;, jsonData, time.Hour)

// Get cache
val, err := rdb.Get(ctx, &quot;product_123&quot;).Result()
if err == redis.Nil {
    // Cache miss, fetch from DB
} else {
    // Cache hit, use val
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-cache-invalidation-the-hard-part&quot;&gt;⚖️ Cache Invalidation: The Hard Part&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;“There are only two hard things in Computer Science: cache invalidation and naming things.” – Phil Karlton&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Whenever the underlying data changes, you need to ensure the cache is updated to avoid stale data.&lt;/p&gt;
&lt;h4 id=&quot;strategies-&quot;&gt;Strategies:&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;✅ Time-based expiration (TTL):&lt;/strong&gt;&lt;br&gt;
Set a cache expiry time so data auto-expires after a fixed duration, ensuring old data clears automatically.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ Manual invalidation:&lt;/strong&gt;&lt;br&gt;
Manually clear or update cache entries whenever the underlying data changes to keep data fresh.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ Write-through caching:&lt;/strong&gt;&lt;br&gt;
Write data to both the cache and the database at the same time, ensuring consistency during reads.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ Cache busting:&lt;/strong&gt;&lt;br&gt;
Change cache keys (e.g., &lt;code&gt;v1:product_123&lt;/code&gt; → &lt;code&gt;v2:product_123&lt;/code&gt;) when your data structure changes to avoid serving stale or incompatible data.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-best-practices-for-caching&quot;&gt;✅ Best Practices for Caching&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Cache &lt;strong&gt;only GET requests&lt;/strong&gt;; avoid caching POST unless explicitly designed.&lt;/li&gt;
&lt;li&gt;Set reasonable TTLs to avoid stale data.&lt;/li&gt;
&lt;li&gt;Monitor cache hit/miss rates to tune cache policies.&lt;/li&gt;
&lt;li&gt;Be cautious caching user-specific or sensitive data.&lt;/li&gt;
&lt;li&gt;Use cache versioning (&lt;code&gt;v1:product_123&lt;/code&gt;) to manage structure changes.&lt;/li&gt;
&lt;li&gt;Document what is cached, where, and why.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-common-pitfalls&quot;&gt;⚠️ Common Pitfalls&lt;/h3&gt;
&lt;p&gt;🚫 Caching sensitive user data publicly.&lt;br&gt;
🚫 Forgetting to invalidate cache on data updates.&lt;br&gt;
🚫 Over-caching dynamic endpoints, leading to stale or incorrect UI.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-future-of-caching-cdn-amp-edge-caching&quot;&gt;🔮 Future of Caching: CDN &amp;amp; Edge Caching&lt;/h3&gt;
&lt;p&gt;Content Delivery Networks (CDNs) like &lt;strong&gt;Cloudflare, AWS CloudFront, Vercel Edge&lt;/strong&gt; cache data closer to the user.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;br&gt;
✅ Lower latency.&lt;br&gt;
✅ Offload origin server traffic.&lt;br&gt;
✅ Built-in stale-while-revalidate strategies.&lt;/p&gt;
&lt;p&gt;Using edge caching in modern React + Go apps can drastically improve performance globally.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-final-thoughts&quot;&gt;🎯 Final Thoughts&lt;/h3&gt;
&lt;p&gt;Caching isn’t just an optimization; it’s essential for building &lt;strong&gt;scalable, reliable, and fast applications.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Whether it’s &lt;strong&gt;browser caching, React Query in the frontend, or Redis in the backend&lt;/strong&gt;, knowing when and how to cache makes you a stronger developer.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;💬 &lt;strong&gt;Have you implemented caching in your apps? What caching challenges have you faced? Let’s discuss in the comments!&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30544/caching-strategies-for-web-apps-from-browser-to-backend</guid>
<pubDate>Sat, 10 Oct 2026 03:33:24 +0000</pubDate>
</item>
<item>
<title>My side project's free tiers were dying, so I counted what each service actually bills</title>
<link>https://coderlegion.com/30349/my-side-projects-free-tiers-were-dying-so-i-counted-what-each-service-actually-bills</link>
<description>&lt;p&gt;Galaxy Movies is a movie discovery side project ~ Next.js Pages Router on Vercel, TMDB for data, Upstash Redis for caching &amp;amp; rate limiting, and OpenAI for a chat assistant &amp;amp; generated synopses. Everything runs on free tiers, which is the whole point of a side project.&lt;/p&gt;
&lt;p&gt;A few weeks ago all three of those free tiers started groaning at once. Vercel function usage was climbing, Upstash was burning through its daily command quota well before midnight, and OpenAI spend had a suspicious correlation with traffic spikes that produced no actual readers.&lt;/p&gt;
&lt;p&gt;The fixes all came down to the same realization: &lt;strong&gt;every &quot;free&quot; tier bills a different unit, and I was counting the wrong things.&lt;/strong&gt; Here's what was actually billing me, and what I changed.&lt;/p&gt;
&lt;h3 id=&quot;act-1-the-bots-were-running-up-my-function-bill&quot;&gt;Act 1: The bots were running up my function bill&lt;/h3&gt;
&lt;p&gt;Vercel bills function &lt;em&gt;duration&lt;/em&gt;, and my movie and TV detail pages were expensive: five parallel TMDB fetches (title, watch providers, credits, ratings, recommendations), plus Redis reads for the AI content slots, all in &lt;code&gt;getServerSideProps&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The expensive traffic wasn't humans. SEO crawlers and AI scrapers sweep the entire TMDB ID space in bulk, and every one of those detail pages ran the full fetch chain. They don't index anything and send zero readers. Pure cost.&lt;/p&gt;
&lt;p&gt;The fix was a thin render path just for them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Bots that don't index get a minimal render. They sweep the TMDB id
// space in bulk and don't need providers, credits, videos, or AI slots.
if (isBot(ua) &amp;amp;&amp;amp; !isSearchEngine(ua)) {
  const titleRes = await fetch(`https://api.themoviedb.org/3/${media}/${id}?api_key=${apiKey}`)
  // on failure return notFound or errorProps; otherwise:
  context.res.setHeader('Cache-Control', 'private, no-store')
  return { props: { ...thinProps, media, title, topCast: [] } }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two details matter here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;isSearchEngine&lt;/code&gt; split.&lt;/strong&gt; &lt;code&gt;isBot&lt;/code&gt; matches a broad allowlist, but Googlebot, Bingbot, and DuckDuckGo still get the full render since they index. Social preview bots get the thin version. Whether a bot indexes is the only question that matters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;private, no-store&lt;/code&gt; is critical.&lt;/strong&gt; If the stripped page entered the shared CDN cache, a cache collision could serve the bot version to a human, or worse, to Googlebot. Cheap responses must never contaminate the real cache.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also found &lt;code&gt;robots.txt&lt;/code&gt; was being rendered by a Next.js &lt;em&gt;page&lt;/em&gt;, which meant every crawler's very first request was a function invocation. It moved to a static &lt;code&gt;public/robots.txt&lt;/code&gt;, which doubles as an enforcement layer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;User-agent: AhrefsBot
User-agent: SemrushBot
User-agent: MJ12bot
User-agent: Bytespider
User-agent: GPTBot
User-agent: ClaudeBot
User-agent: CCBot
# ...several more
Disallow: /
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yes, &lt;code&gt;Disallow&lt;/code&gt; is an honor system, but the big commercial crawlers honor it, and they were most of the volume.&lt;/p&gt;
&lt;p&gt;Finally, I lengthened the SSR CDN cache windows: &lt;code&gt;s-maxage&lt;/code&gt; from 1 hour to 1 day, &lt;code&gt;stale-while-revalidate&lt;/code&gt; from 1 day to 1 week. Repeat crawler hits on popular titles now terminate at the edge instead of running SSR again.&lt;/p&gt;
&lt;h3 id=&quot;act-2-upstash-bills-every-single-redis-command&quot;&gt;Act 2: Upstash bills every single Redis command&lt;/h3&gt;
&lt;p&gt;This one caught me off guard. Upstash's free tier bills &lt;em&gt;per command&lt;/em&gt;, and a pipeline doesn't save you, because &lt;strong&gt;each member of a pipeline is its own billed command&lt;/strong&gt;. My request paths fanned out into Redis calls that felt free because &quot;Redis is fast.&quot;&lt;/p&gt;
&lt;p&gt;The audit turned up some embarrassing finds. Cache reads on detail pages were ~97% misses, so I was paying a billed command for a null almost every time. Rate limiting cost up to three commands per check. A dataset updated once a day was being fetched again on every pageview.&lt;/p&gt;
&lt;p&gt;The fixes, roughly in order of satisfaction:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rate limiting became one Lua script.&lt;/strong&gt; &lt;code&gt;INCRBY&lt;/code&gt; plus a conditional &lt;code&gt;EXPIRE&lt;/code&gt; plus a TTL recovery check run inside a single &lt;code&gt;EVAL&lt;/code&gt;, billed once no matter how many &lt;code&gt;redis.call()&lt;/code&gt;s it makes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lua&quot;&gt;local count = redis.call(&quot;INCRBY&quot;, KEYS[1], ARGV[1])
if count &amp;lt;= tonumber(ARGV[1]) then
  redis.call(&quot;EXPIRE&quot;, KEYS[1], ARGV[2])
elseif count &amp;gt; tonumber(ARGV[3]) and redis.call(&quot;TTL&quot;, KEYS[1]) &amp;lt; 0 then
  redis.call(&quot;EXPIRE&quot;, KEYS[1], ARGV[2])
end
return count
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The TTL check handles a real edge case: if a key somehow loses its expiry, a blocked client fixes it on the next attempt instead of being rate limited forever.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check caches before paying the rate limit write.&lt;/strong&gt; &lt;code&gt;/api/prices&lt;/code&gt; was rate limiting requests that would have been cache hits, paying a write before doing a read. Reorder, and every cache hit is one command instead of two.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Memoize what changes daily.&lt;/strong&gt; The &lt;code&gt;streaming-changes&lt;/code&gt; diffs are rewritten once a day by cron but were being fetched per key on every pageview. A per instance LRU holding results for ten minutes turns repeat page loads into zero Redis commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const changesMemo = new LRUCache&amp;lt;string, Record&amp;lt;string, ProviderChanges&amp;gt;&amp;gt;({ max: 64, ttl: 10 * 60 * 1000 })

export async function getProviderChanges(providerKeys = [], { fresh = false } = {}) {
  const memoKey = [...providerKeys].sort().join(',')
  if (!fresh) {
    const memoized = changesMemo.get(memoKey)
    if (memoized) return memoized
  }
  // ...Redis reads, then changesMemo.set(memoKey, changes)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;fresh&lt;/code&gt; flag exists because the cron itself must never match subscribers against a diff it just replaced. Memoization that's correct &lt;em&gt;most&lt;/em&gt; of the time is a bug with extra steps.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Batch the counters.&lt;/strong&gt; Every &lt;code&gt;HINCRBY&lt;/code&gt; is billed, so AI usage and analytics deltas accumulate in a &lt;code&gt;Map&lt;/code&gt; and flush every 30 seconds (or 50 fields). Stats lag by up to half a minute and a dying instance loses its unflushed tail, a fine trade for telemetry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Restructure data to match access patterns.&lt;/strong&gt; Detail pages fetch both AI content slots in one &lt;code&gt;MGET&lt;/code&gt; instead of two &lt;code&gt;GET&lt;/code&gt;s. Generated lists moved from one key per slug into a single hash, where &lt;code&gt;HGETALL&lt;/code&gt; is one command replacing an unbounded key fanout.&lt;/p&gt;
&lt;p&gt;Net result: roughly half the daily command volume, with quota headroom for the first time in weeks.&lt;/p&gt;
&lt;h3 id=&quot;act-3-openai-spend-or-why-is-a-scraper-generating-my-content-&quot;&gt;Act 3: OpenAI spend, or &quot;why is a scraper generating my content?&quot;&lt;/h3&gt;
&lt;p&gt;The AI features on detail pages (generated synopses, similar title blurbs) trigger an OpenAI call on a cache miss. Guess who was causing cache misses at scale: the same scrapers from Act 1. Bots were literally generating my content.&lt;/p&gt;
&lt;p&gt;So bots got read only access: they can consume AI content that's already cached but can never trigger generation (&lt;code&gt;generate: false&lt;/code&gt; on the cache lookup). Human traffic is unaffected; scraper sweeps now cost zero tokens.&lt;/p&gt;
&lt;p&gt;The other fixes were less dramatic but added up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Failed cron seeds stopped retrying forever.&lt;/strong&gt; A broken seed now records &lt;code&gt;failed&lt;/code&gt; with an attempt counter and gives up after 3 tries, instead of burning a &lt;code&gt;discover&lt;/code&gt; call on every daily run until the heat death of the universe.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Longer cache TTLs.&lt;/strong&gt; Reviews and similar titles are static per title, so 365 day TTLs mean one generation per title, ever.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompt dieting.&lt;/strong&gt; Similar title prompts dropped to 6 candidates with 200 character overviews; editorial prompts got explicit word budgets; structured JSON output removed the &quot;please don't ramble&quot; tax.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared chat cache.&lt;/strong&gt; Identical chat requests (quick actions, common first questions; the bulk of traffic) now hit a Redis cache shared across instances, plus tighter limits: 10/hr per IP, 1000/day global.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per task model control.&lt;/strong&gt; All OpenAI calls route through one module with per task &lt;code&gt;reasoning_effort&lt;/code&gt;: &lt;code&gt;none&lt;/code&gt; for latency sensitive tasks, &lt;code&gt;low&lt;/code&gt; only for the discover generator. Real token usage gets logged so I can see where spend actually goes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-kill-switch&quot;&gt;The kill switch&lt;/h4&gt;
&lt;p&gt;The scariest scenario isn't gradual spend. It's a quota or billing problem silently converting every request into an error, or worse, into retry storms. Billing and auth failures (&lt;code&gt;401&lt;/code&gt;/&lt;code&gt;402&lt;/code&gt;, &lt;code&gt;insufficient_quota&lt;/code&gt;) now trip a shared kill switch with a one hour cooldown:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export function isQuotaError(err: unknown): boolean {
  if (!(err instanceof OpenAI.APIError)) return false
  if (err.status === 401 || err.status === 402) return true
  const code = err.code || err.error?.code
  return code === 'insufficient_quota'
      || code === 'billing_hard_limit_reached'
      || code === 'billing_not_active'
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While it's tripped: &lt;code&gt;/api/chat&lt;/code&gt; returns &lt;code&gt;503 { code: 'ai_unavailable' }&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; spending budget, the chat widget checks &lt;code&gt;/api/aiStatus&lt;/code&gt; on mount and hides itself entirely instead of showing an error bubble, the generators skip their calls, and the content cron skips the run &lt;em&gt;without&lt;/em&gt; burning seed attempts, since an outage isn't a seed's fault. Crucially, ordinary &lt;code&gt;429 rate_limit_exceeded&lt;/code&gt; errors don't trip it; those are transient.&lt;/p&gt;
&lt;p&gt;One deliberate choice throughout: &lt;strong&gt;degrade in the direction that saves money.&lt;/strong&gt; When Redis errors on a check that protects spend, the code falls back to a per instance limiter and never fails open. An in memory limiter might be less accurate, but a Redis outage can't be used to run up my OpenAI bill.&lt;/p&gt;
&lt;h3 id=&quot;the-part-where-i-broke-things&quot;&gt;The part where I broke things&lt;/h3&gt;
&lt;p&gt;Honest bookkeeping: the cost cuts shipped two bugs worth their own paragraph.&lt;/p&gt;
&lt;p&gt;After moving the chat cache to Redis, identical requests started hitting on every quick action prompt, and the cache hit path returned plain JSON that the SSE client never parsed. Cache hits went from &quot;the fast path&quot; to &quot;the broken path&quot; overnight. Fix: return cache hits through the SSE stream like everything else.&lt;/p&gt;
&lt;p&gt;And when bots stopped triggering AI generation, Lighthouse CI got caught in the crossfire. It audits the real detail pages, and its UA matched the bot list, so it was suddenly measuring the thin render. &lt;code&gt;Chrome-Lighthouse&lt;/code&gt; went on the search engine side of the check.&lt;/p&gt;
&lt;p&gt;Both bugs were the same shape: &lt;strong&gt;a filter added at the edge changed behavior for a consumer I forgot about.&lt;/strong&gt; If your bot detection doesn't have a list of &lt;em&gt;legitimate&lt;/em&gt; automated consumers, it will find one for you. In production. On a Sunday.&lt;/p&gt;
&lt;h3 id=&quot;what-i-d-tell-past-me&quot;&gt;What I'd tell past me&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Count the billed unit, not the abstraction.&lt;/strong&gt; &quot;Redis call,&quot; &quot;function invocation,&quot; and &quot;API request&quot; are all wrong. It's commands, gigabyte seconds, and tokens, and the gap between what I thought I was spending and what I was billed lived entirely inside that distinction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Segment bots by whether they index&lt;/strong&gt;, not whether they're bots. That one boolean decided which requests deserved the expensive render, the Redis reads, and the cacheable output.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache at the cheapest layer that works.&lt;/strong&gt; CDN edge &amp;gt; shared Redis &amp;gt; per instance LRU &amp;gt; regenerate. I had requests traversing the whole hierarchy when a static file would do (looking at you, &lt;code&gt;robots.txt&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free tier quotas are architecture constraints&lt;/strong&gt;, not billing details. &quot;A few Redis commands per request is fine&quot; is a statement that only survives until you do the multiplication.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instrument spend itself.&lt;/strong&gt; The usage counters I added to survive the Upstash quota are now how I watch OpenAI costs. The fix for a quota crisis becomes your observability for the next one.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The site is &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.galaxymovies.app&quot;&gt;galaxymovies.app&lt;/a&gt; if you want to see the thin renders in the wild. View source as a human, please; the bots are expensive enough. Happy to compare notes in the comments if you've fought the same free tier math!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30349/my-side-projects-free-tiers-were-dying-so-i-counted-what-each-service-actually-bills</guid>
<pubDate>Thu, 08 Oct 2026 22:42:45 +0000</pubDate>
</item>
<item>
<title>Architecting a Zero-Loss Webhook Ingestion Engine with n8n and Redis (5k req/s DLQ)</title>
<link>https://coderlegion.com/29955/architecting-a-zero-loss-webhook-ingestion-engine-with-n8n-and-redis-5k-req-s-dlq</link>
<description>&lt;h2 id=&quot;architecting-a-zero-loss-webhook-ingestion-engine-with-n8n-and-redis-5k-req-s-dlq-&quot;&gt;Architecting a Zero-Loss Webhook Ingestion Engine with n8n and Redis (5k req/s DLQ)&lt;/h2&gt;
&lt;p&gt;Webhooks are inherently fragile. If an upstream payment gateway emits an invoice event during a scheduled database restart, or if your downstream CRM hits an unannounced rate limit (HTTP 429), standard automation flows drop the execution. &lt;/p&gt;
&lt;p&gt;By default, native webhook nodes in low-code orchestration engines (like standard n8n workflows, Zapier, or Make) execute synchronously. If downstream services timeout or throw an uncaught 5xx error, the payload vanishes into log oblivion. &lt;/p&gt;
&lt;p&gt;In this technical guide, we will design and deploy a &lt;strong&gt;Self-Healing Webhook Dead-Letter Queue (DLQ) &amp;amp; Payload Recovery Engine&lt;/strong&gt; using &lt;strong&gt;n8n&lt;/strong&gt; and &lt;strong&gt;Redis&lt;/strong&gt;. This architecture isolates the ingestion layer from downstream consumers, enforces exponential backoff with full jitter, preserves metadata, and guarantees exactly-once processing using SHA-256 idempotency checks.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-bottleneck-the-flaws-of-synchronous-webhook-ingestion&quot;&gt;1. The Bottleneck: The Flaws of Synchronous Webhook Ingestion&lt;/h3&gt;
&lt;p&gt;Most teams route incoming webhooks directly to their target business logic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[External Provider] ---&amp;gt; [Sync HTTP Endpoint (n8n)] ---&amp;gt; [Process / Parse] ---&amp;gt; [Update Postgres / HubSpot]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This simple pattern breaks catastrophically in production due to three distinct failure modes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Unbuffered Burst Traffic:&lt;/strong&gt; Black Friday or marketing flash sales can turn a typical 10 req/s stream into 3,000 req/s. Synchronous engines run out of Node.js event loop capacity or database connection pools, dropping connections with &lt;code&gt;ECONNRESET&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Cascading 429 Problem:&lt;/strong&gt; When an external CRM starts returning &lt;code&gt;429 Too Many Requests&lt;/code&gt;, naive execution loops retry immediately or sequentially, worsening the rate limit and burning through workflow execution quotas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Payload Loss on Uncaught Exceptions:&lt;/strong&gt; If schema validation fails midway or an unhandled null parameter crashes an execution node, standard error handlers rarely isolate the raw payload for stateful replays without human intervention.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Commercial managed queues (like AWS SQS FIFO or Google Cloud Pub/Sub paired with Lambda) solve this, but they introduce vendor lock-in, complex IAM setup, and high maintenance costs when all you need is a deterministic, observable queue integrated into your n8n workflow ecosystem.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-the-architecture-decoupled-buffering-amp-replay-engine&quot;&gt;2. The Architecture: Decoupled Buffering &amp;amp; Replay Engine&lt;/h3&gt;
&lt;p&gt;To achieve true zero-loss architecture, we decouple ingestion from processing using Redis streams and sorted sets (&lt;code&gt;ZSET&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                           +---------------------------------------+
                           |          Ingestion Layer              |
[Webhook Provider] ------&amp;gt; |  n8n Webhook Node (202 Accepted)      |
                           |  Compute SHA-256 Idempotency Key      |
                           |  Push to Redis Stream (Ingestion Bus) |
                           +---------------------------------------+
                                              |
                                              v
                           +---------------------------------------+
                           |          Worker / Consumer            |
                           |  Read from Stream (XREADGROUP)        |
                           |  Attempt Processing &amp;amp; Routing         |
                           +---------------------------------------+
                                      /               \
                            [Success]/                 \[Failure / 429 / 5xx]
                                    v                   v
                           +----------------+   +---------------------------------------+
                           |  Acknowledge   |   |        Retry Pipeline (ZSET)          |
                           |  (XACK)        |   |  Retry Count &amp;lt; Max?                   |
                           +----------------+   |  Calculate Exponential Backoff+Jitter |
                                                +---------------------------------------+
                                                        |                      |
                                                 [Under Limit]            [Max Retries]
                                                        |                      |
                                                        v                      v
                                                +----------------+   +-------------------+
                                                | Re-enqueue to  |   | Isolated DLQ      |
                                                | Stream at T+d  |   | Payload Sanitized |
                                                +----------------+   | Metadata Attached |
                                                                     +-------------------+
                                                                               |
                                                                               v
                                                                     +-------------------+
                                                                     | Auto/Manual Replay|
                                                                     | (Idempotent Sync) |
                                                                     +-------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;pipeline-stages&quot;&gt;Pipeline Stages&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Ingestion &amp;amp; De-duplication:&lt;/strong&gt; The webhook accepts the payload, generates a SHA-256 hash of the deterministic payload body, and issues a Redis &lt;code&gt;SETNX&lt;/code&gt; lock to prevent duplicate deliveries. The caller immediately receives &lt;code&gt;202 Accepted&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buffer Stream:&lt;/strong&gt; Payloads enter a Redis Stream (&lt;code&gt;webhook:stream:raw&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consumer &amp;amp; Error Catching:&lt;/strong&gt; n8n worker loops consume batches. If an upstream call fails with a retryable code (&lt;code&gt;408&lt;/code&gt;, &lt;code&gt;429&lt;/code&gt;, &lt;code&gt;500&lt;/code&gt;, &lt;code&gt;502&lt;/code&gt;, &lt;code&gt;503&lt;/code&gt;, &lt;code&gt;504&lt;/code&gt;), it moves to a scheduled retry set.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exponential Backoff with Full Jitter:&lt;/strong&gt; Instead of retrying on static intervals, the pipeline computes:&lt;br&gt;
\[\text{Delay} = \min(\text{MaxDelay}, \text{Base} \times 2^{\text{attempt}}) \times \text{random}(0.5, 1.5)\]&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Isolated DLQ:&lt;/strong&gt; After $N$ failed retries (e.g., 5 attempts), the payload is sent to &lt;code&gt;webhook:dlq:dead&lt;/code&gt;, sanitizing sensitive headers (e.g., &lt;code&gt;Authorization&lt;/code&gt;) while preserving headers, execution error stacks, and original timestamps.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-the-code-amp-logic&quot;&gt;3. The Code &amp;amp; Logic&lt;/h3&gt;
&lt;p&gt;Below are the core n8n JavaScript Code node snippets that drive this engine.&lt;/p&gt;
&lt;h4 id=&quot;a-deterministic-idempotency-key-amp-header-sanitization&quot;&gt;A. Deterministic Idempotency Key &amp;amp; Header Sanitization&lt;/h4&gt;
&lt;p&gt;Add this to an n8n Code Node right after the incoming Webhook Node:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const crypto = require('crypto');

// 1. Extract payload and key headers
const payload = $json.body || $json;
const headers = $json.headers || {};

// 2. Remove non-deterministic variables (signatures, dynamic timestamps)
const sanitizedHeaders = { ...headers };
delete sanitizedHeaders['x-signature-timestamp'];
delete sanitizedHeaders['authorization'];

// 3. Create deterministic SHA-256 hash from stringified sorted payload
function getDeterministicHash(obj) {
  const sortedKeys = Object.keys(obj).sort();
  const normalizedString = JSON.stringify(obj, sortedKeys);
  return crypto.createHash('sha256').update(normalizedString).digest('hex');
}

const idempotencyKey = headers['idempotency-key'] || getDeterministicHash(payload);
const traceId = `trace_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;

return [{
  json: {
    idempotencyKey,
    traceId,
    originalPayload: payload,
    metadata: {
      receivedAt: new Date().toISOString(),
      originIp: headers['x-forwarded-for'] || 'internal',
      contentType: headers['content-type']
    }
  }
}];
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;b-exponential-backoff-with-jitter-computation&quot;&gt;B. Exponential Backoff with Jitter Computation&lt;/h4&gt;
&lt;p&gt;When a downstream service fails, route the payload to this calculation node before pushing to the Redis delay pool:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Inputs from execution metadata
const currentAttempt = ($json.metadata &amp;amp;&amp;amp; $json.metadata.retryCount) ? $json.metadata.retryCount + 1 : 1;
const MAX_RETRIES = 5;
const BASE_BACKOFF_MS = 2000;
const MAX_BACKOFF_MS = 300000; // 5 minutes

if (currentAttempt &amp;gt; MAX_RETRIES) {
  return [{
    json: {
      ...$json,
      status: 'DLQ_EXHAUSTED',
      finalFailureReason: $json.lastError || 'Max retries exceeded',
      movedToDlqAt: new Date().toISOString()
    }
  }];
}

// Exponential Backoff: Base * 2^(attempt - 1)
const exponentialDelay = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * Math.pow(2, currentAttempt - 1));

// Apply Full Jitter: Uniform distribution between 0.5 and 1.5
const jitterFactor = 0.5 + Math.random();
const finalDelayMs = Math.floor(exponentialDelay * jitterFactor);
const nextExecutionTime = Date.now() + finalDelayMs;

return [{
  json: {
    ...$json,
    status: 'PENDING_RETRY',
    metadata: {
      ...$json.metadata,
      retryCount: currentAttempt,
      nextExecutionTime,
      delayAppliedMs: finalDelayMs
    }
  }
}];
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;c-atomic-redis-deduplication-amp-lock&quot;&gt;C. Atomic Redis Deduplication &amp;amp; Lock&lt;/h4&gt;
&lt;p&gt;Use an n8n Redis node configured with the &lt;strong&gt;Raw Command&lt;/strong&gt; action to ensure the worker process doesn't duplicate executions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;SET webhook:lock:{{ $json.idempotencyKey }} &quot;1&quot; NX EX 86400
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the command returns &lt;code&gt;null&lt;/code&gt;, the payload has already been ingested within the last 24 hours. The n8n router can short-circuit the execution immediately with a &lt;code&gt;200 OK&lt;/code&gt; (Duplicate Ignored), eliminating downstream race conditions.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-deployment-performance-amp-failure-recovery&quot;&gt;4. Deployment, Performance &amp;amp; Failure Recovery&lt;/h3&gt;
&lt;h4 id=&quot;handling-high-concurrency-in-n8n&quot;&gt;Handling High Concurrency in n8n&lt;/h4&gt;
&lt;p&gt;By default, n8n writes execution histories to its main database. When operating at peak loads, you must tune execution modes to avoid bottlenecking SQLite/PostgreSQL:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Set Execution Data Pruning:&lt;/strong&gt;&lt;br&gt;
Set &lt;code&gt;EXECUTIONS_DATA_SAVE_ON_SUCCESS=none&lt;/code&gt; in your n8n environment variables to prevent your disk from filling up with successful 200-ACK payloads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable Queue Mode:&lt;/strong&gt;&lt;br&gt;
Deploy n8n in &lt;strong&gt;Queue Mode&lt;/strong&gt; using Redis as the broker. Decouple your webhook listener instances (Webhook Processors) from your Worker instances (Consumers).&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# Production n8n environment variables configuration snippet
EXECUTIONS_MODE=queue
QUEUE_BULL_REDIS_HOST=redis-master.internal
QUEUE_BULL_REDIS_PORT=6379
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
EXECUTIONS_DATA_SAVE_ON_ERROR=all
EXECUTIONS_DATA_SAVE_ON_SUCCESS=none
EXECUTIONS_DATA_PRUNE=true
EXECUTIONS_DATA_MAX_AGE=72
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-automated-self-healing-replay-pipeline&quot;&gt;The Automated Self-Healing Replay Pipeline&lt;/h4&gt;
&lt;p&gt;When downstream platforms recover from an outage, the Dead-Letter Queue contains hundreds of valid, pending payloads. Manually copying payloads out of log files is prone to human error.&lt;/p&gt;
&lt;p&gt;The recovery engine handles this using a scheduled or trigger-based replay workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pulls batches from the Redis set &lt;code&gt;webhook:dlq:dead&lt;/code&gt; via &lt;code&gt;ZRANGEBYSCORE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Verifies the target system's health via an HTTP OPTIONS/GET health-check endpoint.&lt;/li&gt;
&lt;li&gt;Upon a valid 200 response from the target system, it resets &lt;code&gt;retryCount&lt;/code&gt; to 0 and re-injects payloads directly into the primary consumer stream.&lt;/li&gt;
&lt;li&gt;Idempotency guarantees (&lt;code&gt;SETNX&lt;/code&gt;) ensure that partially completed side-effects are not duplicated.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-conclusion-amp-ready-to-use-workflow&quot;&gt;5. Conclusion &amp;amp; Ready-to-use Workflow&lt;/h3&gt;
&lt;p&gt;Handling webhooks reliably requires moving away from fragile, synchronous direct-routing architectures. By introducing an in-memory Redis buffer with jittered exponential backoffs, metadata preservation, and automated dead-letter queueing, you can handle thousands of requests per second without losing business-critical data.&lt;/p&gt;
&lt;p&gt;You can implement this system using the code snippets and architecture outlined in this article. &lt;/p&gt;
&lt;p&gt;If you prefer a pre-built solution, you can get the production-ready package with test fixtures, container orchestration scripts, and end-to-end replay pipelines ready to import:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Instant Access on Whop:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://whop.com/mr-studio-7faa/self-healing-webhook-dead-letter-queue-payload-recovery-engine-n8n-redis&quot;&gt;Download the Production Workflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct Download on Gumroad:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://mannyverse767.gumroad.com/l/bcdvfg&quot;&gt;Download via Gumroad&lt;/a&gt; — use promo code &lt;strong&gt;&lt;code&gt;EARLYBIRD&lt;/code&gt;&lt;/strong&gt; for 20% off.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/29955/architecting-a-zero-loss-webhook-ingestion-engine-with-n8n-and-redis-5k-req-s-dlq</guid>
<pubDate>Tue, 06 Oct 2026 08:26:44 +0000</pubDate>
</item>
<item>
<title>Why I Used BullMQ + Redis for Background Jobs in My NestJS Application — and When I Wouldn't</title>
<link>https://coderlegion.com/29737/why-i-used-bullmq-redis-for-background-jobs-in-my-nestjs-application-and-when-i-wouldnt</link>
<description>&lt;p&gt;While working on &lt;strong&gt;Waitless&lt;/strong&gt;, a real-time queue management platform, I came across a backend problem that looked simple at first:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;A customer reaches the front of the queue, and the system needs to send notifications to other customers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The first instinct could be to simply send the email inside the HTTP request.&lt;/p&gt;
&lt;p&gt;But once I looked at the problem as a backend architecture problem rather than just an implementation task, I realized that sending the email and handling the user's request are two different workloads.&lt;/p&gt;
&lt;p&gt;That led me to &lt;strong&gt;BullMQ + Redis&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The interesting part wasn't learning the API. The interesting part was understanding &lt;strong&gt;why this architecture made sense, what Redis was actually doing, and when I would choose something else instead.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-in-waitless&quot;&gt;The problem in Waitless&lt;/h3&gt;
&lt;p&gt;Waitless allows customers to join a digital queue and track their position without creating an account.&lt;/p&gt;
&lt;p&gt;When one customer is called, another customer may need to receive a notification such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“It's your turn.”&lt;/li&gt;
&lt;li&gt;“You're almost next.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I already had the queue state stored in PostgreSQL. The question was:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Should the API send the email immediately, or should it create background work?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A synchronous approach could look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;HTTP request
     ↓
NestJS
     ↓
PostgreSQL
     ↓
Send email
     ↓
HTTP response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem is that sending an email depends on an external service.&lt;/p&gt;
&lt;p&gt;If that service is slow or temporarily unavailable, my API request is affected.&lt;/p&gt;
&lt;p&gt;I don't necessarily need the email to be completed before responding to the user.&lt;/p&gt;
&lt;p&gt;What I actually need is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Remember that this email has to be sent, and process it separately.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is exactly the kind of problem a job queue is designed to solve.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;introducing-bullmq&quot;&gt;Introducing BullMQ&lt;/h2&gt;
&lt;p&gt;BullMQ provides a job queue abstraction for Node.js applications.&lt;/p&gt;
&lt;p&gt;Instead of doing the work immediately, the application creates a job:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Application
     ↓
   Queue
     ↓
    Job
     ↓
   Worker
     ↓
 Actual work
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application that creates the job is the &lt;strong&gt;producer&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The worker that processes the job is the &lt;strong&gt;consumer/worker&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This gives me a clean separation between:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Creating work
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Executing work
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That separation becomes particularly useful when the work is slow, retryable, delayed, or needs to be processed independently from the HTTP request.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;where-redis-comes-in&quot;&gt;Where Redis comes in&lt;/h2&gt;
&lt;p&gt;One thing I initially had to clarify for myself was the relationship between BullMQ and Redis.&lt;/p&gt;
&lt;p&gt;They are not the same thing.&lt;/p&gt;
&lt;p&gt;A simplified mental model is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;NestJS
   ↓
BullMQ
   ↓
Redis
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BullMQ is the queue system.&lt;/p&gt;
&lt;p&gt;Redis is the backend that BullMQ uses to store and coordinate queue-related state.&lt;/p&gt;
&lt;p&gt;In my NestJS application, I configure the connection like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;BullModule.forRoot({
  connection: {
    host: 'localhost',
    port: 6379,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Port &lt;code&gt;6379&lt;/code&gt; is Redis's default port, so this configuration tells BullMQ which Redis instance to connect to.&lt;/p&gt;
&lt;p&gt;The important distinction is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;BullMQ is not “running on port 6379.”&lt;br&gt;
BullMQ is connecting to the Redis server running there.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;creating-a-notification-job&quot;&gt;Creating a notification job&lt;/h2&gt;
&lt;p&gt;In Waitless, I inject the notification queue into my service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;@InjectQueue('notifications')
private notifQueue: Queue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now I can add jobs to that queue.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;if (next.email) {
  await this.notifQueue.add('your-turn-now', {
    email: next.email,
    name: next.name,
    queueName: queue.name,
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This does not mean:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Send the email now.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It means:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Create a job called &lt;code&gt;your-turn-now&lt;/code&gt; and put it into the notifications queue.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The payload contains the information the worker will need later.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;NestJS
   ↓
notifQueue.add(...)
   ↓
BullMQ
   ↓
Redis
   ↓
notifications queue
   ↓
your-turn-now job
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The actual email is the responsibility of the worker.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;delayed-jobs&quot;&gt;Delayed jobs&lt;/h2&gt;
&lt;p&gt;One of the features I needed was delayed notification.&lt;/p&gt;
&lt;p&gt;If another customer is second in line, I want to notify them shortly before their turn.&lt;/p&gt;
&lt;p&gt;My implementation looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const secondInLine =
  await this.entryRepo.findSecondWaiting(queueId, next.id);

if (secondInLine?.email) {
  await this.notifQueue.add(
    'your-turn-soon',
    {
      email: secondInLine.email,
      name: secondInLine.name,
      position: 2,
      queueName: queue.name,
      trackingUrl: `http://localhost:3000/track/${secondInLine.token}`,
    },
    {
      delay: 30_000,
    },
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;{ delay: 30_000 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The job doesn't need to be processed immediately.&lt;/p&gt;
&lt;p&gt;The architecture becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Customer becomes second in line
            ↓
      Create BullMQ job
            ↓
       Delayed job
            ↓
          30 sec
            ↓
        Worker picks it up
            ↓
        Send notification
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is much more reliable than simply doing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;setTimeout(() =&amp;gt; {
  sendEmail();
}, 30_000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A timer belongs to the application process itself. A queue gives the work an explicit lifecycle outside the original request.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-not-just-use-postgresql-for-the-queue-&quot;&gt;Why not just use PostgreSQL for the queue?&lt;/h2&gt;
&lt;p&gt;This was one of the more interesting questions I explored.&lt;/p&gt;
&lt;p&gt;At first glance, PostgreSQL looks like a perfectly reasonable choice.&lt;/p&gt;
&lt;p&gt;I could create a table such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;jobs
-------------------
id
type
payload
status
created_at
processed_at
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and continuously insert and delete jobs.&lt;/p&gt;
&lt;p&gt;The problem is that the queue workload has different characteristics from my core business data.&lt;/p&gt;
&lt;p&gt;My PostgreSQL database is responsible for things such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;businesses
queues
customers
queue entries
relationships
transactions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those are my core relational business data.&lt;/p&gt;
&lt;p&gt;The queue workload is different:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;job created
job becomes active
job succeeds
job fails
job retries
job is removed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a &lt;strong&gt;high-churn operational workload&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;With PostgreSQL's MVCC model, repeatedly inserting and deleting rows creates dead tuples that later have to be cleaned up by VACUUM.&lt;/p&gt;
&lt;p&gt;That doesn't mean PostgreSQL cannot implement queues. It absolutely can.&lt;/p&gt;
&lt;p&gt;The real architectural question is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Do I want the queue workload competing with my transactional business workload inside the same database?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For a small application, the answer might be yes.&lt;/p&gt;
&lt;p&gt;For a higher-throughput system, separating those workloads can become valuable.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-redis-made-sense-for-my-queue&quot;&gt;Why Redis made sense for my queue&lt;/h2&gt;
&lt;p&gt;Redis is designed around very fast operations on in-memory data structures.&lt;/p&gt;
&lt;p&gt;That makes it a natural fit for workloads such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;caching&lt;/li&gt;
&lt;li&gt;counters&lt;/li&gt;
&lt;li&gt;temporary state&lt;/li&gt;
&lt;li&gt;queues&lt;/li&gt;
&lt;li&gt;rate limiting&lt;/li&gt;
&lt;li&gt;pub/sub&lt;/li&gt;
&lt;li&gt;fast lookups&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For my notification queue, the important characteristics were:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Asynchronous
High frequency
Short-lived job state
Low latency
Retries
Delayed execution
Worker concurrency
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;PostgreSQL
   ├── business data
   └── queue workload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I could separate them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;PostgreSQL
     ↓
Business state


Redis
     ↓
BullMQ
     ↓
Background jobs
     ↓
Workers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That separation was more important to me than simply saying:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Redis is faster.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-i-learned-about-workload-driven-architecture&quot;&gt;What I learned about workload-driven architecture&lt;/h2&gt;
&lt;p&gt;This changed the way I think about databases.&lt;/p&gt;
&lt;p&gt;The question shouldn't simply be:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Which database is better?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A better question is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;What workload am I dealing with?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Requirement                    &lt;/th&gt;&lt;th&gt;Possible choice&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Relational transactional data  &lt;/td&gt;&lt;td&gt;PostgreSQL&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Very fast temporary state      &lt;/td&gt;&lt;td&gt;Redis&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Background jobs with retries   &lt;/td&gt;&lt;td&gt;BullMQ + Redis&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Document-oriented event data   &lt;/td&gt;&lt;td&gt;MongoDB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Large event streams and replay &lt;/td&gt;&lt;td&gt;Kafka&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Small simple background jobs   &lt;/td&gt;&lt;td&gt;PostgreSQL or a scheduler&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Managed cloud queue            &lt;/td&gt;&lt;td&gt;SQS or similar service&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;These aren't absolute rules.&lt;/p&gt;
&lt;p&gt;They are starting points for architectural reasoning.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;bullmq-gives-me-more-than-a-queue&quot;&gt;BullMQ gives me more than a queue&lt;/h2&gt;
&lt;p&gt;Another thing I learned is that the real value of a job system is not just putting something into a list.&lt;/p&gt;
&lt;p&gt;BullMQ provides concepts such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Jobs
Workers
Retries
Backoff
Delayed jobs
Concurrency
Rate limiting
Failed jobs
Job lifecycle
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That matters because real systems fail.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Worker
   ↓
Send email
   ↓
Email provider unavailable
   ↓
Job fails
   ↓
Retry
   ↓
Worker tries again
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The system can therefore treat background work as something with a lifecycle instead of a simple function call.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;but-there-is-an-important-failure-model&quot;&gt;But there is an important failure model&lt;/h2&gt;
&lt;p&gt;A queue doesn't automatically mean:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“The job runs exactly once.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A worker can crash.&lt;/p&gt;
&lt;p&gt;A network call can time out.&lt;/p&gt;
&lt;p&gt;A job can be retried.&lt;/p&gt;
&lt;p&gt;The same operation may therefore be executed more than once.&lt;/p&gt;
&lt;p&gt;That means job handlers should often be designed with &lt;strong&gt;idempotency&lt;/strong&gt; in mind.&lt;/p&gt;
&lt;p&gt;For example, a payment operation is much more dangerous to retry blindly than sending a harmless notification.&lt;/p&gt;
&lt;p&gt;This was an important lesson for me:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Using a queue changes the failure model of the application.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You have to think about retries, duplicates, timeouts, and partial failures.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;could-i-use-bullmq-without-redis-&quot;&gt;Could I use BullMQ without Redis?&lt;/h2&gt;
&lt;p&gt;Interestingly, yes.&lt;/p&gt;
&lt;p&gt;Modern BullMQ also provides a PostgreSQL backend.&lt;/p&gt;
&lt;p&gt;That means the conceptual architecture can also be:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;NestJS
   ↓
BullMQ
   ↓
PostgreSQL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can make sense when I want to keep the infrastructure simpler and the queue workload is moderate.&lt;/p&gt;
&lt;p&gt;But there is a trade-off.&lt;/p&gt;
&lt;p&gt;Using PostgreSQL for both workloads means:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;PostgreSQL
 ├── transactional business data
 └── queue workload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the queue and business operations share database resources.&lt;/p&gt;
&lt;p&gt;That can be perfectly reasonable for a smaller application.&lt;/p&gt;
&lt;p&gt;For a system with large queue throughput, isolating the queue workload may become more attractive.&lt;/p&gt;
&lt;p&gt;This is why there isn't one universally correct answer.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;when-i-would-not-use-bullmq-redis&quot;&gt;When I would NOT use BullMQ + Redis&lt;/h2&gt;
&lt;p&gt;This was probably the most useful part of the exercise.&lt;/p&gt;
&lt;p&gt;I don't want to add Redis and BullMQ just because they are popular technologies.&lt;/p&gt;
&lt;p&gt;For something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const result = calculateTax(data);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;there is no reason to create a queue.&lt;/p&gt;
&lt;p&gt;If a task takes a few milliseconds and the user needs the result immediately, synchronous processing is simpler.&lt;/p&gt;
&lt;p&gt;I also might avoid BullMQ for a very small scheduled task:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Every day at 08:00
Generate one report
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A simple scheduler could be enough.&lt;/p&gt;
&lt;p&gt;And if the application already uses PostgreSQL and has only a small number of background jobs, a PostgreSQL-backed solution may be simpler than adding another infrastructure component.&lt;/p&gt;
&lt;p&gt;The lesson is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Don't introduce distributed infrastructure without a workload that justifies it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-decision-i-now-make-before-choosing-a-queue&quot;&gt;The decision I now make before choosing a queue&lt;/h2&gt;
&lt;p&gt;Instead of thinking:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Should I use BullMQ?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I ask:&lt;/p&gt;
&lt;h4 id=&quot;1-does-this-work-need-to-happen-asynchronously-&quot;&gt;1. Does this work need to happen asynchronously?&lt;/h4&gt;
&lt;p&gt;If yes, continue.&lt;/p&gt;
&lt;h4 id=&quot;2-does-it-need-retries-or-failure-recovery-&quot;&gt;2. Does it need retries or failure recovery?&lt;/h4&gt;
&lt;p&gt;If yes, a real job system becomes more useful.&lt;/p&gt;
&lt;h4 id=&quot;3-does-it-need-delayed-execution-&quot;&gt;3. Does it need delayed execution?&lt;/h4&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Do this after 30 seconds.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;4-does-it-need-multiple-workers-or-concurrency-&quot;&gt;4. Does it need multiple workers or concurrency?&lt;/h4&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Queue
 ├── Worker 1
 ├── Worker 2
 └── Worker 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;5-does-the-workload-justify-another-infrastructure-component-&quot;&gt;5. Does the workload justify another infrastructure component?&lt;/h4&gt;
&lt;p&gt;This is the trade-off question.&lt;/p&gt;
&lt;p&gt;Only after answering those questions would I choose the technology.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;my-current-mental-model&quot;&gt;My current mental model&lt;/h2&gt;
&lt;p&gt;The biggest thing I learned from implementing this feature is that architecture is less about memorizing technologies and more about matching &lt;strong&gt;workloads to the right tools&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For Waitless, my current architecture can be thought of as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                    Waitless
                       │
          ┌────────────┼────────────┐
          ↓            ↓            ↓
     PostgreSQL      Redis       MongoDB
          │            │            │
   business state    jobs      event history
                    BullMQ
                       │
                    Workers
                       │
                   Email/API
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PostgreSQL answers:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What is the current business state?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Redis + BullMQ answers:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What work needs to be processed asynchronously?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;MongoDB answers:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What happened in the event history, and what can I derive from those events?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The important thing is not that I used three technologies.&lt;/p&gt;
&lt;p&gt;The important thing is that each technology was chosen for a different workload.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-takeaway&quot;&gt;Final takeaway&lt;/h2&gt;
&lt;p&gt;The best lesson I took from this wasn't actually “how to use BullMQ.”&lt;/p&gt;
&lt;p&gt;It was this:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Before choosing a technology, understand the workload.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Ask:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What problem am I solving?
What are the access patterns?
What guarantees do I need?
How does the system fail?
What are the trade-offs?
What simpler alternative could work?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once I started thinking this way, Redis, BullMQ, PostgreSQL, MongoDB and other technologies stopped looking like isolated tools.&lt;/p&gt;
&lt;p&gt;They became different tools for different engineering problems.&lt;/p&gt;
&lt;p&gt;And that is the mindset I want to continue developing as I work toward designing larger backend systems.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29737/why-i-used-bullmq-redis-for-background-jobs-in-my-nestjs-application-and-when-i-wouldnt</guid>
<pubDate>Sun, 04 Oct 2026 15:45:42 +0000</pubDate>
</item>
<item>
<title>Redis Caching Strategies for High-Performance APIs</title>
<link>https://coderlegion.com/27011/redis-caching-strategies-for-high-performance-apis</link>
<description>&lt;h2 id=&quot;redis-caching-strategies-for-high-performance-apis&quot;&gt;Redis Caching Strategies for High-Performance APIs&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The Cache That Made My API Faster—and Introduced a New Problem.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When I first started thinking seriously about API performance, my instinct was simple:&lt;/p&gt;
&lt;p&gt;If the database is slow, optimize the database.&lt;/p&gt;
&lt;p&gt;Add indexes. Improve the query. Tune the connection pool.&lt;/p&gt;
&lt;p&gt;And sometimes, that's exactly what you should do.&lt;/p&gt;
&lt;p&gt;But eventually, you run into a different problem.&lt;/p&gt;
&lt;p&gt;The database isn't necessarily slow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You're just asking it the same question too many times.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Imagine an endpoint that returns product information.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET /api/products/123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The product changes once every few hours, but the endpoint gets thousands of requests every minute.&lt;/p&gt;
&lt;p&gt;Without caching, every request goes straight to PostgreSQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Request
   ↓
API
   ↓
PostgreSQL
   ↓
Response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database is doing the same work over and over again.&lt;/p&gt;
&lt;p&gt;That's where Redis comes in.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-simple-idea-behind-caching&quot;&gt;The Simple Idea Behind Caching&lt;/h2&gt;
&lt;p&gt;Instead of asking the database every time, we store frequently requested data temporarily.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Request
   ↓
Redis
   ↓
  HIT ─────→ Response
   │
  MISS
   ↓
Database
   ↓
Redis
   ↓
Response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first request pays the database cost.&lt;/p&gt;
&lt;p&gt;The next requests don't.&lt;/p&gt;
&lt;p&gt;In a Node.js API, a simple cache-aside implementation looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const key = `product:${productId}`;

const cached = await redis.get(key);

if (cached) {
  return JSON.parse(cached);
}

const product = await prisma.product.findUnique({
  where: { id: productId },
});

if (!product) {
  throw new Error(&quot;Product not found&quot;);
}

await redis.set(
  key,
  JSON.stringify(product),
  { EX: 300 }
);

return product;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's the basic pattern:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check Redis → cache miss → query database → store result → return.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Simple.&lt;/p&gt;
&lt;p&gt;But this is where caching stops being just a performance optimization.&lt;/p&gt;
&lt;p&gt;Because now we have two copies of the data.&lt;/p&gt;
&lt;p&gt;And they can disagree.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-stale-data-problem&quot;&gt;The Stale Data Problem&lt;/h2&gt;
&lt;p&gt;Suppose Redis contains:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;product:123
price: ₦45,000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then someone updates the product:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Database
price: ₦50,000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis still contains:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;₦45,000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the cache has a five-minute TTL, users could receive the old price for another five minutes.&lt;/p&gt;
&lt;p&gt;This is the real challenge with caching.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Making data fast to access is easy. Keeping it correct is harder.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That's why TTL alone isn't enough.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;ttl-vs-invalidation&quot;&gt;TTL vs Invalidation&lt;/h2&gt;
&lt;p&gt;TTL answers:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;How long should this cache entry exist?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Invalidation answers:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&quot;When should I remove it because the underlying data changed?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;await prisma.product.update({
  where: { id: productId },
  data: updateData,
});

await redis.del(`product:${productId}`);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the next request becomes a cache miss and retrieves the latest value from the database.&lt;/p&gt;
&lt;p&gt;For many APIs, &lt;strong&gt;cache-aside + TTL + delete-on-write&lt;/strong&gt; is a very practical starting point.&lt;/p&gt;
&lt;p&gt;You don't need a complicated caching architecture on day one.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;then-there-s-the-cache-stampede&quot;&gt;Then There's the Cache Stampede&lt;/h2&gt;
&lt;p&gt;Here's where things get interesting.&lt;/p&gt;
&lt;p&gt;Imagine a popular cache entry expires:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;product:123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the exact same time, 1,000 requests arrive.&lt;/p&gt;
&lt;p&gt;They all check Redis:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;MISS
MISS
MISS
MISS
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And suddenly all 1,000 requests hit PostgreSQL.&lt;/p&gt;
&lt;p&gt;The cache that was supposed to protect the database has just created a database traffic spike.&lt;/p&gt;
&lt;p&gt;This is commonly called a &lt;strong&gt;cache stampede&lt;/strong&gt; or &lt;strong&gt;thundering herd&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For extremely hot keys, you can prevent this by allowing only one request to rebuild the cache while the others wait.&lt;/p&gt;
&lt;p&gt;Redis can help here through distributed locking.&lt;/p&gt;
&lt;p&gt;But I wouldn't introduce locking everywhere.&lt;/p&gt;
&lt;p&gt;If a query takes 10ms, the complexity may not be worth it.&lt;/p&gt;
&lt;p&gt;If rebuilding the cache takes several seconds under heavy traffic, it's a different conversation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Optimize for the workload you actually have.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-should-you-cache-&quot;&gt;What Should You Cache?&lt;/h2&gt;
&lt;p&gt;This is probably the most important question.&lt;/p&gt;
&lt;p&gt;I wouldn't cache something simply because Redis is available.&lt;/p&gt;
&lt;p&gt;Good candidates usually have three characteristics:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Frequently requested
+
Expensive to retrieve
+
Safe to be slightly stale
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Product details
Public profiles
Search results
Configuration
Reference data
Expensive aggregations
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I'd be much more careful with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Account balances
Payment state
Authorization decisions
Inventory
Transaction state
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Especially in financial systems.&lt;/p&gt;
&lt;p&gt;If a wallet contains ₦100,000 and Redis says ₦100,000 while the database says ₦70,000 after a transaction, using that cached value for a financial decision is dangerous.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fast wrong data is still wrong data.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For critical state, the database should remain authoritative.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;your-cache-key-is-part-of-your-architecture&quot;&gt;Your Cache Key Is Part of Your Architecture&lt;/h2&gt;
&lt;p&gt;A cache key isn't just a random string.&lt;/p&gt;
&lt;p&gt;This:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;is a terrible cache key.&lt;/p&gt;
&lt;p&gt;This is better:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;user:123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And in a multi-tenant system:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;tenant:456:user:123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For filtered results:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;products:category:phones:page:1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every parameter that changes the response needs to be represented appropriately in the key.&lt;/p&gt;
&lt;p&gt;Otherwise, you can end up returning the wrong cached response to the right request.&lt;/p&gt;
&lt;p&gt;That's a correctness bug—not a performance bug.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;redis-should-accelerate-your-system-not-become-your-system&quot;&gt;Redis Should Accelerate Your System, Not Become Your System&lt;/h2&gt;
&lt;p&gt;One of the most important design decisions is what happens when Redis fails.&lt;/p&gt;
&lt;p&gt;For a cache-aside architecture, I'd rather have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Redis available
    ↓
Use cache

Redis unavailable
    ↓
Use database
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The API might become slower.&lt;/p&gt;
&lt;p&gt;But it remains functional.&lt;/p&gt;
&lt;p&gt;That's a much better failure mode than:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Redis unavailable
    ↓
Entire API unavailable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your application completely depends on Redis just to retrieve data that already exists in PostgreSQL, you've accidentally turned your cache into a critical dependency.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;measure-the-difference&quot;&gt;Measure the Difference&lt;/h2&gt;
&lt;p&gt;Finally, don't add Redis and declare victory.&lt;/p&gt;
&lt;p&gt;Measure it.&lt;/p&gt;
&lt;p&gt;Before caching:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Average latency: 400ms
P95 latency:     800ms
DB queries/sec:  2,000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After caching:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Average latency: 70ms
P95 latency:     150ms
DB queries/sec:  500
Cache hit rate:  85%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you have evidence.&lt;/p&gt;
&lt;p&gt;The goal isn't to make Redis busy.&lt;/p&gt;
&lt;p&gt;The goal is to make the &lt;strong&gt;entire system do less unnecessary work.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-bigger-lesson&quot;&gt;The Bigger Lesson&lt;/h2&gt;
&lt;p&gt;Redis isn't what makes an API fast.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avoiding unnecessary work makes an API fast.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Redis is simply one of the tools that helps you do that.&lt;/p&gt;
&lt;p&gt;The moment you introduce a cache, you're making a trade:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;More memory
+
More complexity
+
Potentially stale data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;in exchange for:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Lower latency
+
Less database load
+
Higher throughput
+
Better scalability
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The best caching strategy isn't the one that puts the most data into Redis.&lt;/p&gt;
&lt;p&gt;It's the one that &lt;strong&gt;eliminates the most unnecessary work while keeping the system correct.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That's the difference between simply &lt;strong&gt;using Redis&lt;/strong&gt; and actually &lt;strong&gt;designing with Redis&lt;/strong&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27011/redis-caching-strategies-for-high-performance-apis</guid>
<pubDate>Sat, 12 Sep 2026 22:30:40 +0000</pubDate>
</item>
<item>
<title>Rate Limiting in the Next.js App Router: Field Notes on Middleware, Redis, and the Server Action That Looks Like Every Other POST</title>
<link>https://coderlegion.com/25091/rate-limiting-router-field-notes-middleware-redis-server-action-that-looks-like-every-other</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; An in-memory rate limiter is wrong on serverless because every function instance keeps its own counter, so a limit of 10 requests per minute becomes 10 requests per minute &lt;em&gt;per instance&lt;/em&gt;. The second trap is that every Next.js Server Action POSTs to the URL of the page that called it, so path-based limiting in middleware cannot tell one action from another.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A rate limiter is a counter with a deadline: allow N requests per identity per time window and reject the rest with HTTP 429. I have written that in ten lines before and been happy with it. Moving the same idea into a Next.js 16 App Router project on Vercel broke it in three places I did not expect — where the counter lives, who counts as an identity, and how you attach a limit to a Server Action that has no URL of its own.&lt;/p&gt;
&lt;h3 id=&quot;key-takeaways&quot;&gt;Key takeaways&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;An in-memory &lt;code&gt;Map&lt;/code&gt; rate limiter is per-instance on serverless. With ten warm function instances, a 10-requests-per-minute limit permits 100 requests per minute.&lt;/li&gt;
&lt;li&gt;Next.js middleware is the cheapest place to shed abusive traffic because it runs before the route's own function boots, but it also sees RSC prefetch requests that carry the &lt;code&gt;RSC: 1&lt;/code&gt; header and that the user never intentionally made.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NextRequest.ip&lt;/code&gt; was removed in Next.js 15. On Vercel, read the client address with &lt;code&gt;ipAddress(request)&lt;/code&gt; from &lt;code&gt;@vercel/functions&lt;/code&gt; instead of trusting a raw &lt;code&gt;x-forwarded-for&lt;/code&gt; header.&lt;/li&gt;
&lt;li&gt;Every Next.js Server Action POSTs to the current page URL and carries a build-generated &lt;code&gt;Next-Action&lt;/code&gt; header, so the only reliable place to limit a specific action is inside the action body.&lt;/li&gt;
&lt;li&gt;Reject with status 429 and a &lt;code&gt;Retry-After&lt;/code&gt; header, and decide fail-open versus fail-closed per route &lt;em&gt;before&lt;/em&gt; your Redis has its first outage.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-does-an-in-memory-rate-limiter-break-on-serverless-&quot;&gt;Why does an in-memory rate limiter break on serverless?&lt;/h3&gt;
&lt;p&gt;A module-scope &lt;code&gt;Map&lt;/code&gt; is private to one function instance, and a serverless platform runs many instances at once. Each instance therefore enforces the full limit on its own, so the effective limit is your configured limit multiplied by the number of warm instances. That number is not something you control or can observe from inside the request.&lt;/p&gt;
&lt;p&gt;This is the code I have shipped and regretted:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Do not ship this to a serverless runtime.
const hits = new Map&amp;lt;string, { count: number; reset: number }&amp;gt;();

export function limit(key: string, max = 10, windowMs = 60_000) {
  const now = Date.now();
  const entry = hits.get(key);
  if (!entry || entry.reset &amp;lt; now) {
    hits.set(key, { count: 1, reset: now + windowMs });
    return { ok: true };
  }
  entry.count += 1;
  return { ok: entry.count &amp;lt;= max };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vercel Fluid Compute makes this harder to notice rather than easier. Fluid Compute reuses a single function instance across concurrent requests instead of spawning one per request, so the &lt;code&gt;Map&lt;/code&gt; survives far longer than it did under classic serverless. In local development and in a quiet preview deployment the limiter looks correct. It only comes apart under traffic spread across enough instances to matter, and nothing in the logs announces it.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Map&lt;/code&gt; also never shrinks. On a long-lived instance every unique key you have ever seen stays resident until the instance is recycled, which is a slow memory leak wearing a rate limiter costume.&lt;/p&gt;
&lt;p&gt;The fix is not a cleverer &lt;code&gt;Map&lt;/code&gt;. The counter has to live in a store that every instance shares and that supports an atomic increment: Redis, or any datastore with a compare-and-set primitive.&lt;/p&gt;
&lt;h3 id=&quot;should-the-limit-run-in-middleware-or-in-the-route-handler-&quot;&gt;Should the limit run in middleware or in the route handler?&lt;/h3&gt;
&lt;p&gt;Both, for different jobs. Middleware runs before Next.js resolves the route, so a request rejected there never boots the route's function and never touches your database. That makes middleware the correct place for a coarse, identity-agnostic abuse limit. The route handler knows the authenticated user, the parsed body, and the business meaning of the call, which makes it the correct place for a per-user quota.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
import { ipAddress } from '@vercel/functions';
import { ratelimit } from '@/lib/ratelimit';

export const config = {
  matcher: ['/api/:path*', '/login', '/signup'],
};

export async function middleware(request: NextRequest) {
  // RSC prefetches fire on link hover. The user did not ask for these.
  if (request.headers.get('rsc') === '1') return NextResponse.next();

  const ip = ipAddress(request) ?? '127.0.0.1';
  const { success, limit, remaining, reset } = await ratelimit.limit(`ip:${ip}`);

  if (success) return NextResponse.next();

  return NextResponse.json(
    { error: 'rate_limited', message: 'Too many requests.' },
    {
      status: 429,
      headers: {
        'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)),
        'X-RateLimit-Limit': String(limit),
        'X-RateLimit-Remaining': String(remaining),
        'X-RateLimit-Reset': String(reset),
      },
    },
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;rsc&lt;/code&gt; header check is the line I added after watching my own quota drain while I did nothing but move a mouse. The Next.js router prefetches linked routes on hover and on viewport entry, and those requests are real HTTP requests that hit middleware with an &lt;code&gt;RSC: 1&lt;/code&gt; header. Counting them means a user who scrolls a navigation-heavy page is rate limited before they click anything.&lt;/p&gt;
&lt;p&gt;Since Next.js 15.5 you can also opt middleware into the Node.js runtime with &lt;code&gt;export const config = { runtime: 'nodejs' }&lt;/code&gt;, which lets you use a normal Redis client there. I still prefer an HTTP-based store in middleware, because middleware sits on the latency path of every matched request.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Placement &lt;/th&gt;&lt;th&gt;Sees &lt;/th&gt;&lt;th&gt;Cost of a rejection &lt;/th&gt;&lt;th&gt;Use it for&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Middleware &lt;/td&gt;&lt;td&gt;URL, headers, cookies, IP &lt;/td&gt;&lt;td&gt;Lowest — route function never boots &lt;/td&gt;&lt;td&gt;IP-level abuse and brute-force shielding&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Route handler &lt;/td&gt;&lt;td&gt;Everything, including session and body &lt;/td&gt;&lt;td&gt;Function has already started &lt;/td&gt;&lt;td&gt;Per-user quotas, per-endpoint cost control&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Server Action body &lt;/td&gt;&lt;td&gt;Session, typed arguments &lt;/td&gt;&lt;td&gt;Function has already started &lt;/td&gt;&lt;td&gt;Form submissions and mutations&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;which-algorithm-should-i-actually-use-&quot;&gt;Which algorithm should I actually use?&lt;/h3&gt;
&lt;p&gt;Pick the cheapest algorithm whose failure mode you can live with.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fixed window&lt;/strong&gt; keeps one counter per window, so a check is a single &lt;code&gt;INCR&lt;/code&gt;. Its flaw is the boundary: ten requests at 11:59:59 and ten more at 12:00:00 pass a &quot;ten per minute&quot; limit while delivering twenty requests in one second.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sliding window log&lt;/strong&gt; stores a timestamp per request and is exact, but its memory grows with your traffic, which is the wrong direction for a defence against traffic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sliding window counter&lt;/strong&gt; weights the previous window by how much of it still overlaps the current one. Bounded memory, no boundary burst, and my default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token bucket&lt;/strong&gt; gives each identity a capacity and a refill rate, so a quiet client may spend saved tokens at once. Right for APIs whose clients legitimately batch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;@upstash/ratelimit&lt;/code&gt; package ships all four as &lt;code&gt;fixedWindow&lt;/code&gt;, &lt;code&gt;slidingWindow&lt;/code&gt;, &lt;code&gt;tokenBucket&lt;/code&gt;, and &lt;code&gt;cachedFixedWindow&lt;/code&gt;. If you write your own against Redis, the thing to get right is atomicity. &lt;code&gt;INCR&lt;/code&gt; followed by a separate &lt;code&gt;EXPIRE&lt;/code&gt; is two round trips, and if the process dies between them you have created a key with no expiry, which locks that identity out permanently. Do it in one script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lua&quot;&gt;-- One round trip. The TTL is set only on the first hit of a window.
local current = redis.call('INCR', KEYS[1])
if current == 1 then
  redis.call('PEXPIRE', KEYS[1], ARGV[1])
end
return current
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;how-do-i-identify-the-client-when-everything-sits-behind-a-proxy-&quot;&gt;How do I identify the client when everything sits behind a proxy?&lt;/h3&gt;
&lt;p&gt;A rate limit is only as good as its key. &lt;code&gt;NextRequest.ip&lt;/code&gt; and &lt;code&gt;NextRequest.geo&lt;/code&gt; were removed in Next.js 15, so reading &lt;code&gt;request.ip&lt;/code&gt; is now a type error rather than a subtle wrong answer. On Vercel the replacement is &lt;code&gt;ipAddress(request)&lt;/code&gt; from &lt;code&gt;@vercel/functions&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Do not key on a raw &lt;code&gt;x-forwarded-for&lt;/code&gt; header unless you are certain a trusted proxy overwrites it. &lt;code&gt;x-forwarded-for&lt;/code&gt; is an ordinary request header, so on any origin reachable directly, an attacker sets it to a new value per request and gets an unlimited number of fresh limit buckets. That is a complete bypass, not a partial failure.&lt;/p&gt;
&lt;p&gt;IPv6 needs its own rule. A single residential subscriber is routinely assigned an entire /64 prefix, so keying on the full 128-bit address hands one attacker 2^64 distinct identities.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function identityKey(request: NextRequest, userId?: string) {
  // A stable account beats a network address whenever you have one.
  if (userId) return `user:${userId}`;

  const ip = ipAddress(request) ?? '0.0.0.0';
  if (ip.includes(':')) {
    // One subscriber can own a whole /64. Key the prefix, not the address.
    return `ip6:${ip.split(':').slice(0, 4).join(':')}`;
  }
  return `ip4:${ip}`;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;how-do-i-rate-limit-a-server-action-when-every-action-is-a-post-to-the-same-url-&quot;&gt;How do I rate limit a Server Action when every action is a POST to the same URL?&lt;/h3&gt;
&lt;p&gt;A Server Action is a function marked with the &lt;code&gt;'use server'&lt;/code&gt; directive that the client invokes over the network. The invocation is an HTTP POST to the URL of the page the action was called from, with a &lt;code&gt;Next-Action&lt;/code&gt; header containing a build-generated identifier for that specific action. There is no dedicated route path, which is exactly what breaks the obvious approach.&lt;/p&gt;
&lt;p&gt;In middleware, a POST from a contact form on &lt;code&gt;/contact&lt;/code&gt; and a POST from a delete-account button on &lt;code&gt;/contact&lt;/code&gt; are the same URL and the same method. You can read &lt;code&gt;request.headers.get('next-action')&lt;/code&gt; to at least distinguish action POSTs from ordinary document requests, but that identifier is a hash that changes when the build changes, so branch on its presence and never on its value.&lt;/p&gt;
&lt;p&gt;The reliable place is inside the action:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;'use server';

import { headers } from 'next/headers';
import { auth } from '@/lib/auth';
import { ratelimit } from '@/lib/ratelimit';

export async function sendMessage(_prev: State, formData: FormData): Promise&amp;lt;State&amp;gt; {
  const session = await auth();
  const key = session?.user.id ?? (await headers()).get('x-forwarded-for') ?? 'anon';

  const { success, reset } = await ratelimit.limit(`action:sendMessage:${key}`);
  if (!success) {
    const seconds = Math.ceil((reset - Date.now()) / 1000);
    // Return a value. A thrown error reaches production as an opaque digest.
    return { ok: false, error: `Too many messages. Try again in ${seconds}s.` };
  }

  // ...the real work
  return { ok: true };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Returning a value rather than throwing matters. An uncaught error inside a Server Action is redacted in production and surfaces to the client as a generic message with a digest, so the user is told something went wrong instead of being told to wait forty seconds. Rate limiting is a normal outcome, not an exception.&lt;/p&gt;
&lt;h3 id=&quot;what-should-a-429-response-actually-contain-&quot;&gt;What should a 429 response actually contain?&lt;/h3&gt;
&lt;p&gt;HTTP 429 Too Many Requests is the correct status, and &lt;code&gt;Retry-After&lt;/code&gt; is the header that makes a limiter usable by anyone other than a human staring at a browser. Its value is either a number of seconds or an HTTP date.&lt;/p&gt;
&lt;p&gt;Alongside it, emit the limit state. The &lt;code&gt;X-RateLimit-Limit&lt;/code&gt; / &lt;code&gt;X-RateLimit-Remaining&lt;/code&gt; / &lt;code&gt;X-RateLimit-Reset&lt;/code&gt; convention is not a standard but it is what most SDKs already parse. The IETF draft &lt;code&gt;draft-ietf-httpapi-ratelimit-headers&lt;/code&gt; defines &lt;code&gt;RateLimit&lt;/code&gt; and &lt;code&gt;RateLimit-Policy&lt;/code&gt; as structured fields; adopt it only if your consumers understand it.&lt;/p&gt;
&lt;p&gt;Two mistakes I have made and seen: returning 403 for a rate limit, which tells the client to stop forever rather than to retry, and returning 500, which pollutes your error rate with your own defences working correctly.&lt;/p&gt;
&lt;p&gt;Then decide what happens when the store is unreachable, because it will be. Failing open on a login endpoint turns a Redis outage into an open brute-force window. Failing closed on a public read endpoint turns a Redis outage into a full outage of your site. I choose per route: fail closed on authentication and on anything that spends money, fail open on reads.&lt;/p&gt;
&lt;h3 id=&quot;faq&quot;&gt;FAQ&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Can I rate limit in Next.js without Redis?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Only if your app runs as a single long-lived process, such as one container instance. On any serverless or autoscaled deployment the counter must live in a store shared across instances, because in-process state is multiplied by your instance count.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Does Next.js middleware run on RSC prefetch requests?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Yes. Router prefetches are real HTTP requests that match your middleware &lt;code&gt;matcher&lt;/code&gt; and carry the &lt;code&gt;RSC: 1&lt;/code&gt; header. Exclude them from user-facing quotas or they will consume a visitor's budget before the visitor clicks anything.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; How do I get the client IP in Next.js 15 and Next.js 16?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; &lt;code&gt;NextRequest.ip&lt;/code&gt; was removed in Next.js 15. On Vercel, call &lt;code&gt;ipAddress(request)&lt;/code&gt; from &lt;code&gt;@vercel/functions&lt;/code&gt;. Elsewhere, read the forwarded header your own trusted proxy sets and confirm that the proxy overwrites rather than appends it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Should a rate limiter fail open or fail closed when Redis is down?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Decide per route. Fail closed on login, signup, password reset, and payment endpoints, where failing open creates a security window. Fail open on public reads, where failing closed converts a dependency outage into a site outage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Is Vercel BotID a replacement for rate limiting?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; No. Vercel BotID is bot detection, which answers whether a caller is automated. A rate limit answers how often any caller, human or not, may perform an expensive operation. They defend different things and compose well together.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.devya.dev/blogs/rate-limiting-nextjs-app-router-field-notes&quot;&gt;devya.dev&lt;/a&gt;. Also on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.eng-ahmed.com/blog/rate-limiting-nextjs-app-router-field-notes&quot;&gt;eng-ahmed.com&lt;/a&gt;. Built by &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.devya.dev&quot;&gt;Devya Solutions&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25091/rate-limiting-router-field-notes-middleware-redis-server-action-that-looks-like-every-other</guid>
<pubDate>Fri, 21 Aug 2026 13:48:12 +0000</pubDate>
</item>
<item>
<title>Directus + Coolify: Should You Decouple Postgres &amp; Redis?</title>
<link>https://coderlegion.com/24235/directus-coolify-should-you-decouple-postgres-redis</link>
<description>&lt;blockquote&gt;&lt;p&gt;This is Part 2 of the Directus + Coolify series. If you're new here, start with &lt;strong&gt;&quot;Secure Your VPS Before Hackers Do&quot;&lt;/strong&gt; and the first Directus + Coolify post — the bundled, single-Compose-file setup — before following along with this one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;In the first method, we coupled all of the services into one stack using a single Docker Compose file. The network between all services was created automatically, and we didn't have to start them up individually — which removes the risk of a race condition if service startup isn't handled properly.&lt;/p&gt;
&lt;p&gt;If you're running a single app, that's genuinely the recommended way to set up Directus on a Coolify-managed VPS. Going in, I assumed there were several good reasons to split the services apart instead — more control over backups, monitoring, restarts, that kind of thing. So before recommending decoupling, I actually tested each of those assumptions on a live Coolify instance.&lt;/p&gt;
&lt;p&gt;Most of them turned out to be wrong.&lt;/p&gt;
&lt;h3 id=&quot;myth-1-restarting-directus-restarts-the-whole-stack&quot;&gt;Myth 1: Restarting Directus Restarts the Whole Stack&lt;/h3&gt;
&lt;p&gt;I expected that restarting Directus inside the bundled Compose file would restart Redis and Postgres along with it. It doesn't. Coolify lets you restart each service in the stack independently — Directus, Database, and Cache each get their own &lt;strong&gt;Restart&lt;/strong&gt; button, right there in the same view. No decoupling needed for this one.&lt;/p&gt;
&lt;h3 id=&quot;myth-2-you-need-a-separate-database-resource-for-s3-backups&quot;&gt;Myth 2: You Need a Separate Database Resource for S3 Backups&lt;/h3&gt;
&lt;p&gt;Same story. Even with Postgres bundled inside the Directus Compose file, Coolify still gives it its own dedicated &lt;strong&gt;Backups&lt;/strong&gt; option, S3 included. This isn't a separate-resource-only feature.&lt;/p&gt;
&lt;h3 id=&quot;myth-3-scheduled-tasks-require-separate-services&quot;&gt;Myth 3: Scheduled Tasks Require Separate Services&lt;/h3&gt;
&lt;p&gt;Also not true. Coolify exposes a &lt;strong&gt;Scheduled Tasks&lt;/strong&gt; tab per service, even inside a single bundled stack — complete with a &lt;strong&gt;Container name&lt;/strong&gt; dropdown letting you target the cron job at just the database, or just Directus, without splitting anything apart.&lt;/p&gt;
&lt;h3 id=&quot;what-actually-holds-up&quot;&gt;What Actually Holds Up&lt;/h3&gt;
&lt;p&gt;Two things survived testing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First: metrics.&lt;/strong&gt; This one's confirmed directly in Coolify's own documentation — CPU and memory metrics collection is explicitly not available for Docker Compose–based deployments. If you want to see per-container resource usage through Coolify's built-in monitoring, the service needs to be created as its own standalone resource, not bundled inside a Compose file. This is a real, documented limitation of the bundled approach.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second: sharing a database across multiple apps.&lt;/strong&gt; This one isn't a Coolify feature at all — it's just how Docker networking works. A database defined inside one app's Compose file lives on that stack's own private network by default. A second, completely separate application can't reach it without deliberately bridging the two networks. If you've got a desktop app and a mobile app that both need to talk to the same Postgres instance, that database needs to exist as its own standalone resource from the start — it can't stay tucked inside one app's Compose file.&lt;/p&gt;
&lt;p&gt;So this really comes down to two reasons to decouple, not five — one a genuine Coolify limitation, the other a structural fact about Docker networking. Let's set both of those up properly.&lt;/p&gt;
&lt;h3 id=&quot;adding-the-resources&quot;&gt;Adding the Resources&lt;/h3&gt;
&lt;h4 id=&quot;adding-the-postgresql-database&quot;&gt;Adding the PostgreSQL Database&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dashboard → Add Project&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;+ Add Resource&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Databases → PostgreSQL → Supabase PostgreSQL (with extensions)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Change the name to something human-friendly&lt;/li&gt;
&lt;li&gt;Copy your username and password and save them somewhere — you'll need them shortly&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Start&lt;/strong&gt;, and wait for the database to spin up (this can take a little while)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once it's up, the status should read &lt;strong&gt;&quot;Running (Healthy).&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Enable metrics while you're here.&lt;/strong&gt; In the sidebar, go to &lt;strong&gt;Servers → localhost → Metrics&lt;/strong&gt;, and enable metrics. Back in your project, under &lt;strong&gt;Databases&lt;/strong&gt;, click your Postgres database, then &lt;strong&gt;Metrics&lt;/strong&gt; — you should now see live CPU/memory usage for it. This is the exact capability that isn't available on a bundled Compose deployment.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;adding-redis-cache&quot;&gt;Adding Redis Cache&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dashboard → + Add Resource&lt;/strong&gt; (from the project itself)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Databases → Redis&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Rename it to something more convenient&lt;/li&gt;
&lt;li&gt;Copy the Redis connection URL — you'll need it shortly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save&lt;/strong&gt;, then &lt;strong&gt;Start&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once both are running, SSH into your VPS and run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to confirm both containers are up.&lt;/p&gt;
&lt;h4 id=&quot;the-docker-compose-config-for-directus&quot;&gt;The Docker Compose Config for Directus&lt;/h4&gt;
&lt;p&gt;This is what goes into the empty Compose file for the Directus resource:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  directus:
    image: 'directus/directus:12.2.0'
    ports:
      - '8055:8055'
    volumes:
      - './uploads:/directus/uploads'
      - './extensions:/directus/extensions'
    healthcheck:
      test:
        - CMD-SHELL
        - 'wget --spider -q http://127.0.0.1:8055/server/ping || exit 1'
      interval: 10s
      timeout: 5s
      retries: 5
      start_interval: 5s
      start_period: 30s
    environment:
      SECRET: secretstring
      MARKETPLACE_TRUST: all
      DB_CLIENT: pg
      DB_HOST:
      DB_PORT: '5432'
      DB_DATABASE: postgres
      DB_USER: postgres
      DB_PASSWORD:
      CACHE_ENABLED: 'true'
      CACHE_STORE: redis
      CACHE_AUTO_PURGE: 'true'
      REDIS:
      ADMIN_EMAIL: *Emails are not allowed*
      ADMIN_PASSWORD: '1234567890'
      CORS_ENABLED: 'true'
      CORS_ORIGIN: 'true'
      CORS_CREDENTIALS: 'true'
      PUBLIC_URL:
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;Notice the healthcheck already uses &lt;code&gt;127.0.0.1&lt;/code&gt; instead of &lt;code&gt;localhost&lt;/code&gt; — that's the fix from Part 1. Carrying it forward here saves you from hitting the exact same &quot;unhealthy&quot; bug all over again.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;adding-directus&quot;&gt;Adding Directus&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dashboard → + Add Resource&lt;/strong&gt; (from the project itself)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Applications → Docker Compose Empty&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Paste in the Compose configuration above&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network → Connect To Predefined Network&lt;/strong&gt; → check the box&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Services → Directus service → Settings&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Add your Directus subdomain — remember to use &lt;code&gt;https://&lt;/code&gt; (e.g. &lt;code&gt;https://directus.yourdomain.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;connecting-the-services-together&quot;&gt;Connecting the Services Together&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Variable &lt;/th&gt;&lt;th&gt;What it is&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;SECRET&lt;/code&gt; &lt;/td&gt;&lt;td&gt;A long, unguessable random string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;DB_HOST&lt;/code&gt; &lt;/td&gt;&lt;td&gt;The name of your Postgres container — run &lt;code&gt;docker ps&lt;/code&gt; on your VPS to find it&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;DB_DATABASE&lt;/code&gt; &lt;/td&gt;&lt;td&gt;The name of the database on your Postgres server (Coolify's default is usually &lt;code&gt;postgres&lt;/code&gt;, but confirm it against your Postgres service)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;DB_USER&lt;/code&gt; &lt;/td&gt;&lt;td&gt;The database username, from the Postgres service you created earlier&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;DB_PASSWORD&lt;/code&gt; &lt;/td&gt;&lt;td&gt;The password from that same Postgres service&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;REDIS&lt;/code&gt; &lt;/td&gt;&lt;td&gt;The Redis connection URL from the Redis service you created earlier&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;PUBLIC_URL&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Your Directus subdomain&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Redis URL gotcha:&lt;/strong&gt; the connection URL follows the format &lt;code&gt;redis://username:password@host:port&lt;/code&gt;. Coolify's generated URL includes the username you set when creating the Redis resource — in my case, that username was also &lt;code&gt;redis&lt;/code&gt;, so the URL looked like &lt;code&gt;redis://redis:somelongvariable...&lt;/code&gt;. Directus doesn't need the username here, just the password, so strip that segment out: &lt;code&gt;redis://:somelongvariable...&lt;/code&gt;. If you used a different username when creating your Redis resource, remove &lt;em&gt;that&lt;/em&gt; value instead — not literally the word &quot;redis.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;setting-up-environment-variables&quot;&gt;Setting Up Environment Variables&lt;/h3&gt;
&lt;p&gt;Rather than hardcoding any of this directly into the Compose file, move it into Directus's environment variables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dashboard → Projects → Services / Directus&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment Variables&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;+ Add&lt;/strong&gt;, enter the variable name in all caps, and its value&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Repeat for: &lt;code&gt;SECRET&lt;/code&gt;, &lt;code&gt;DB_HOST&lt;/code&gt;, &lt;code&gt;DB_DATABASE&lt;/code&gt;, &lt;code&gt;DB_USER&lt;/code&gt;, &lt;code&gt;DB_PASSWORD&lt;/code&gt;, &lt;code&gt;ADMIN_EMAIL&lt;/code&gt;, &lt;code&gt;ADMIN_PASSWORD&lt;/code&gt;, and &lt;code&gt;REDIS&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;updating-the-compose-file&quot;&gt;Updating the Compose File&lt;/h4&gt;
&lt;p&gt;Now reference those variables instead of the raw values:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;SECRET: '${SECRET}'
DB_HOST: '${DB_HOST}'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...and so on for each variable. Then:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Restart&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;launching-directus&quot;&gt;Launching Directus&lt;/h3&gt;
&lt;p&gt;Paste your Directus subdomain into the browser, and you should land on your running Directus instance. You can also click &lt;strong&gt;Links&lt;/strong&gt; on the Directus service, then click the subdomain — it'll take you straight to your Directus Studio login.&lt;/p&gt;
&lt;p&gt;Same as the last video: sign up for your free license, which arrives by email, and paste it into your Directus instance to unlock everything.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Any questions or hit a different result testing any of these yourself? Drop it in the comments — I'm genuinely curious whether this holds up across different Coolify versions and setups.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24235/directus-coolify-should-you-decouple-postgres-redis</guid>
<pubDate>Thu, 06 Aug 2026 21:39:55 +0000</pubDate>
</item>
<item>
<title>I Build a blog API with Redis - Here's every problem I Hit</title>
<link>https://coderlegion.com/22689/i-build-a-blog-api-with-redis-heres-every-problem-i-hit</link>
<description>&lt;p&gt;I build a blog API with FastAPI + Redis + MySQL.&lt;br&gt;
Three cache problems almost killed my app.&lt;br&gt;
If you also face these problems!&lt;br&gt;
Here's how I fixed each one.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1-cache-penetration&quot;&gt;1. Cache Penetration&lt;/h2&gt;
&lt;h3 id=&quot;what-s-cache-penetration-&quot;&gt;What's Cache Penetration?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Cache Penetration&lt;/strong&gt; refers to a scenario where the requested data exists neither in cache nor in the database,causing every request to hit the database directly,thereby increasing the database load.&lt;/p&gt;
&lt;h3 id=&quot;how-to-solve-&quot;&gt;How to solve?&lt;/h3&gt;
&lt;h4 id=&quot;1-caching-null-values&quot;&gt;1.Caching null values&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Steps:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When a queried key exists in neither the cache nor the database,the result (such as &lt;code&gt;null&lt;/code&gt; or an empty value) is cached with a short TTL.&lt;/li&gt;
&lt;li&gt;This way,subsequent identical requests can be served directly from the cache,avoiding frequent database access.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;...
redis = get_redis()
data = redis.get(cache_key)

# First, check the cache. If the cache entry exists, check whether it is __NULL__
if data is not None:
    if data == &quot;__NULL__&quot;:
        return {&quot;data&quot;: None, &quot;source&quot;: &quot;cache&quot;}
    return {&quot;data&quot;: data, &quot;source&quot;: &quot;cache&quot;}

# Second, cache miss, query database
value = query_database(cache_key)

# Third, cache the result whether it exists in DB or not
if value is not None:
    redis.setex(cache_key, 60, value)          # cache real data
else:
    redis.setex(cache_key, 10, &quot;__NULL__&quot;)     # cache null sentinel (short TTL)

return value

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2-cache-breakdown&quot;&gt;2.Cache breakdown&lt;/h2&gt;
&lt;h3 id=&quot;what-s-cache-breakdown-&quot;&gt;What's Cache breakdown?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Cache breakdown&lt;/strong&gt;occurs when a popular data entry expires in the cache while a massive number of concurrent requests are hitting that same data at the exact same time.All those requests penetrate straight through to the database,putting enormous pressure on it and potentially causing system performance issues.&lt;/p&gt;
&lt;h3 id=&quot;how-to-solve--1&quot;&gt;How to solve?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Lock mechanism:&lt;/strong&gt;When the cache expires,use a locking mechanism to ensure that only one thread can access the database and update the cache.The other threads wait util the cache is rebuilt before reading the data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Generate a lock key based on the given key
lock_key = f&quot;mutex:{key}&quot;

# Use SETNX operation to acquire the lock. If the key does not exist, set it with an expiration time of 300 seconds.
# This prevents concurrent access.
lock_acquired = redis_client.set(lock_key, &quot;1&quot;, nx=True, ex=300)

if lock_acquired:
    try:
        # Successfully acquired the lock, query the database
        value = db_query_func(key)
        if value is not None:
            # Write the result to the cache with an expiration time of 3600 seconds
            redis_client.setex(key, 3600, value)
        return value
    finally:
        # Release the lock
        redis_client.delete(lock_key)
else:
    # Failed to acquire the lock, sleep for 0.1 seconds and then retry
    time.sleep(0.1)
    return get_data_with_mutex(key, redis_client, db_query_func)

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3-cache-avalanche&quot;&gt;3.Cache avalanche&lt;/h2&gt;
&lt;h3 id=&quot;what-s-cache-avalanche-&quot;&gt;What's cache avalanche?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Cache avalance&lt;/strong&gt;refers to a situation where a large amount of cached data expires at the same time or the cache service goes down. As a result, all requests are directly sent to the database, causing a sudden surge in the database's pressure and even leading to its downtime.&lt;/p&gt;
&lt;h3 id=&quot;how-to-solve--2&quot;&gt;How to solve?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Randomized TTL&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The core idea is to avoid a large number of keys expiring simultaneously.&lt;/li&gt;
&lt;li&gt;When setting the expiration time for the cache, add a random value.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import random

expire_time = time + random.randint(0,300)
redis.set(key,value,ex=expire_time)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-about-you-&quot;&gt;What about you?&lt;/h3&gt;
&lt;p&gt;Have you run into any of these cache problems in your projects? &lt;br&gt;
Or do you use a different strategy? Let me know in the comments &lt;/p&gt;
&lt;p&gt;If this was helpful, follow me for more backend deep dives.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22689/i-build-a-blog-api-with-redis-heres-every-problem-i-hit</guid>
<pubDate>Wed, 15 Jul 2026 00:51:31 +0000</pubDate>
</item>
<item>
<title>Your Laravel Isn’t Slow, *Your Architecture Is*.</title>
<link>https://coderlegion.com/21413/your-laravel-isnt-slow-your-architecture-is</link>
<description>
&lt;h2 id=&quot;your-laravel-isnt-slow-your-architecture-is-mastering-scale-with-advanced-octane--reactive-caching&quot;&gt;Your Laravel Isn&amp;rsquo;t Slow, Your Architecture Is: Mastering Scale with Advanced Octane &amp;amp; Reactive Caching&lt;/h2&gt;

&lt;h3&gt;Introduction&lt;/h3&gt;

&lt;p&gt;If your Laravel API is struggling under heavy load, the instinct is often to dive deep into optimizing Eloquent queries or micro-tweaking individual lines of code. While performance profiling remains crucial, this approach often addresses symptoms, not the root cause. The real bottleneck in high-traffic Laravel deployments isn&amp;rsquo;t typically the framework itself; it&amp;rsquo;s the architectural paradigm. We need to shift our focus from optimizing individual operations to optimizing the &lt;em&gt;flow&lt;/em&gt; and &lt;em&gt;state&lt;/em&gt; of our application. This tutorial will guide you through two advanced strategies &amp;ndash; leveraging Octane&amp;rsquo;s asynchronous capabilities and implementing a reactive caching strategy with Redis Streams &amp;ndash; to unlock sustained low-latency throughput that transforms your Laravel application into a true high-performance powerhouse.&lt;/p&gt;

&lt;h3 id=&quot;code-layout--architectural-walkthrough&quot;&gt;Code Layout &amp;amp; Architectural Walkthrough&lt;/h3&gt;

&lt;p&gt;Moving beyond basic Swoole or RoadRunner setups, we&amp;rsquo;re talking about a paradigm shift that integrates background processing directly into the request flow and introduces real-time cache invalidation.&lt;/p&gt;

&lt;h4 id=&quot;1-octanes-asynchronous-power-non-blocking-operations&quot;&gt;1. Octane&amp;rsquo;s Asynchronous Power: Non-Blocking Operations&lt;/h4&gt;

&lt;p&gt;The key here is to release the HTTP response &lt;em&gt;before&lt;/em&gt; non-critical operations complete, leveraging Octane&amp;rsquo;s persistent application state. Laravel&amp;rsquo;s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatchAfterResponse()&lt;/code&gt; is the starting point, but we&amp;rsquo;ll integrate it with custom Octane bootloader hooks to ensure robust, non-blocking execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conceptual Implementation:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Custom Octane Bootloader:&lt;/strong&gt; Create a service provider that registers Octane worker lifecycle hooks. This allows you to perform setup and teardown tasks for each worker process, ensuring resources are ready.&lt;/p&gt;

    &lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// app/Providers/OctaneServiceProvider.php&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;App\Providers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Support\Facades\Octane&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Support\ServiceProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OctaneServiceProvider&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServiceProvider&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Octane&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;onWorkerStart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// Perform expensive, one-time worker setup here&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// e.g., warm up a complex service container dependency&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

        &lt;span class=&quot;nc&quot;&gt;Octane&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;onWorkerError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Throwable&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// Log or handle worker errors gracefully&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// Consider Octane::onWorkerStop for cleanup if necessary&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Asynchronous Dispatch in Controllers/Services:&lt;/strong&gt; In your critical API endpoints, identify operations that don&amp;rsquo;t &lt;em&gt;strictly&lt;/em&gt; need to block the user&amp;rsquo;s response (e.g., audit logging, sending non-critical notifications, syncing data to a third-party service).&lt;/p&gt;

    &lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// app/Http/Controllers/OrderController.php&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;App\Http\Controllers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;App\Jobs\ProcessAuditLog&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;App\Jobs\SyncOrderToCRM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Routing\Controller&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderController&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Controller&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Request&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ... (Validate request, create order, etc.) ...&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$order&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// Dispatch non-critical jobs to the queue *after* response is sent&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// This leverages Octane's persistent connections without blocking the user.&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;ProcessAuditLog&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Order Created'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;afterResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;SyncOrderToCRM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;afterResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'message'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Order created successfully!'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'order_id'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;201&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-&amp;gt;afterResponse()&lt;/code&gt;, Laravel ensures these jobs are pushed to your configured queue (e.g., Redis, database) only &lt;em&gt;after&lt;/em&gt; the HTTP response has been fully sent to the client. Octane&amp;rsquo;s persistent workers ensure that the queue workers are readily available and efficient, minimizing latency overhead for job dispatching.&lt;/p&gt;

&lt;h4 id=&quot;2-reactive-caching-with-redis-streams-for-real-time-invalidation&quot;&gt;2. Reactive Caching with Redis Streams for Real-time Invalidation&lt;/h4&gt;

&lt;p&gt;Simple TTL-based caching often leads to stale data or necessitates aggressive, short TTLs that negate performance benefits. A reactive caching strategy uses Redis Streams to broadcast data changes and trigger real-time invalidation across your data layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conceptual Implementation:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Publisher (Model Event Listener):&lt;/strong&gt; Whenever a critical data model is updated, deleted, or created, an event is published to a Redis Stream.&lt;/p&gt;

    &lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// app/Providers/AppServiceProvider.php (or a dedicated EventServiceProvider)&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;App\Providers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;App\Models\Product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;App\Services\CacheInvalidationService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Support\ServiceProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AppServiceProvider&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServiceProvider&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;CacheInvalidationService&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$cacheInvalidationService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;created&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$cacheInvalidationService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'product_updated'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;updated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$cacheInvalidationService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'product_updated'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deleted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$cacheInvalidationService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'product_deleted'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// app/Services/CacheInvalidationService.php&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;App\Services&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Support\Facades\Redis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CacheInvalidationService&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$streamKey&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'cache_invalidation_stream'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$entityId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Redis&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;xadd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;streamKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'*'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
            &lt;span class=&quot;s1&quot;&gt;'event'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s1&quot;&gt;'entity_id'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$entityId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s1&quot;&gt;'timestamp'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s1&quot;&gt;'data'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;json_encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Consumer (Queue Worker/Octane Worker):&lt;/strong&gt; A dedicated listener consumes this Redis Stream. Upon receiving an event, it knows exactly which cache keys (for complex objects or query results) need to be invalidated.&lt;/p&gt;

    &lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// app/Jobs/ProcessCacheInvalidationStream.php (a long-running Octane-aware consumer)&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;App\Jobs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;App\Services\CacheManagerService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Manages complex cache keys&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Bus\Queueable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Contracts\Queue\ShouldQueue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Foundation\Bus\Dispatchable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Queue\InteractsWithQueue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Queue\SerializesModels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Support\Facades\Redis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Support\Facades\Log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProcessCacheInvalidationStream&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ShouldQueue&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Dispatchable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;InteractsWithQueue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Queueable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SerializesModels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;CacheManagerService&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$cacheManagerService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$streamKey&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'cache_invalidation_stream'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$consumerGroup&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'cache_invalidator_group'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$consumerName&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;gethostname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'-'&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;uniqid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Unique consumer name&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// Create consumer group if it doesn't exist&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;Redis&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;xgroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'CREATE'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$streamKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$consumerGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'MKSTREAM'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// Group likely already exists&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// Continuously read from the stream&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nv&quot;&gt;$messages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Redis&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;xreadgroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'GROUP'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$consumerGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$consumerName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'BLOCK'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'COUNT'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'STREAMS'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$streamKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'&amp;gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;empty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$streamKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$streamKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;nv&quot;&gt;$event&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'event'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
                    &lt;span class=&quot;nv&quot;&gt;$entityId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;int&lt;/span&gt;&amp;lt;span cl&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;/li&gt;&lt;/ol&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/21413/your-laravel-isnt-slow-your-architecture-is</guid>
<pubDate>Fri, 26 Jun 2026 13:35:52 +0000</pubDate>
</item>
<item>
<title>How I Stopped a Race Condition from Creating Free Money in My Wallet System</title>
<link>https://coderlegion.com/20524/how-i-stopped-a-race-condition-from-creating-free-money-in-my-wallet-system</link>
<description>&lt;p&gt;&lt;strong&gt;The Bug That Doesn't Show Up in Testing.&lt;/strong&gt;&lt;br&gt;
When I set out to build a production-grade wallet system, I expected the hard parts to be the database schema, the transaction logic, or the authentication layer.&lt;/p&gt;
&lt;p&gt;What I didn't expect was that the most dangerous bug in the system would have nothing to do with wrong code - it would be about timing.&lt;/p&gt;
&lt;p&gt;This is the story of a race condition that lets users spend money they don't have, why it's invisible until it hits production, and how Redis turned it into a non-issue.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What a Wallet System Actually Does&lt;/strong&gt;&lt;br&gt;
Before we get to the bug, a quick picture of the system. A wallet holds a balance. Users can:&lt;/p&gt;
&lt;p&gt;Credit - add funds&lt;br&gt;
Debit - spend funds&lt;br&gt;
Transfer - move funds to another user&lt;br&gt;
Reverse - undo a transaction&lt;br&gt;
Every operation reads the current balance, validates it, and writes a new transaction record. Simple enough. The problem hides in that gap between reading and writing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Race Condition&lt;/strong&gt;&lt;br&gt;
Here's the scenario. A user has ₦5,000 in their wallet. They're on a slow connection, so they tap &quot;Pay&quot; twice. Two HTTP requests hit the server within milliseconds of each other. Both are trying to debit ₦3,000.&lt;/p&gt;
&lt;p&gt;Without any concurrency protection, this is what happens:&lt;/p&gt;
&lt;p&gt;Request A → reads balance: ₦5,000&lt;br&gt;
Request B → reads balance: ₦5,000&lt;/p&gt;
&lt;p&gt;Request A → ₦5,000 - ₦3,000 = ₦2,000 ✓ writes transaction&lt;br&gt;
Request B → ₦5,000 - ₦3,000 = ₦2,000 ✓ writes transaction&lt;br&gt;
Both requests read ₦5,000. Both pass the balance check. Both write a successful debit. The user just spent ₦6,000 from a ₦5,000 wallet - and the system called both transactions successful.&lt;/p&gt;
&lt;p&gt;This isn't a theoretical edge case. Under any real concurrent load, this happens. And the worst part: it never shows up in testing, because your test runner doesn't fire two real HTTP requests at the same millisecond.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why a Database Transaction Doesn't Save You&lt;/strong&gt;&lt;br&gt;
The instinct here is to reach for a database transaction. Wrap the whole thing in BEGIN / COMMIT and let Postgres sort it out.&lt;/p&gt;
&lt;p&gt;The problem is that the race happens before the database transaction begins. Both requests read the balance from the database (or worse, from a cache), both pass the validation check in application code, and then both open their database transactions. The damage is already done.&lt;/p&gt;
&lt;p&gt;You need a lock at the application layer, before the read even happens.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Fix: Distributed Locking with Redis&lt;/strong&gt;&lt;br&gt;
The solution is to ensure that no two operations on the same wallet can run at the same time. Before any balance operation begins, it must claim exclusive access. If another operation already holds the claim, it retries until the lock is free, or gives up after a timeout.&lt;/p&gt;
&lt;p&gt;This is a distributed lock, and Redis is the right tool for it. Here's the implementation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/utils/lock.utils.ts
import { randomUUID } from &quot;crypto&quot;;
import redisClient from &quot;../services/redis.service&quot;;

const LOCK_TTL_MS = 10_000;
const LOCK_RETRY_COUNT = 50;
const LOCK_RETRY_DELAY_MS = 100;

const RELEASE_SCRIPT = `
  if redis.call(&quot;GET&quot;, KEYS[1]) == ARGV[1] then
    return redis.call(&quot;DEL&quot;, KEYS[1])
  else
    return 0
  end
`;

const acquireLock = async (key: string): Promise&amp;lt;string | null&amp;gt; =&amp;gt; {
  const token = randomUUID();
  const result = await redisClient.set(key, token, &quot;PX&quot;, LOCK_TTL_MS, &quot;NX&quot;);
  return result === &quot;OK&quot; ? token : null;
};

export const withLock = async &amp;lt;T&amp;gt;(userId: string, fn: () =&amp;gt; Promise&amp;lt;T&amp;gt;): Promise&amp;lt;T&amp;gt; =&amp;gt; {
  const key = `wallet:lock:${userId}`;
  let token: string | null = null;

  for (let attempt = 0; attempt &amp;lt;= LOCK_RETRY_COUNT; attempt++) {
    token = await acquireLock(key);
    if (token) break;
    await new Promise((resolve) =&amp;gt; setTimeout(resolve, LOCK_RETRY_DELAY_MS));
  }

  if (!token) throw new Error(&quot;Could not acquire lock — resource is busy&quot;);

  try {
    return await fn();
  } finally {
    await redisClient.eval(RELEASE_SCRIPT, 1, key, token);
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let's break down each decision.&lt;/p&gt;
&lt;p&gt;SET key token PX 10000 NX&lt;br&gt;
This is a single atomic Redis command that does three things at once:&lt;/p&gt;
&lt;p&gt;NX — only set the key if it does not already exist&lt;br&gt;
PX 10000 — expire the key after 10 seconds&lt;br&gt;
The value is a unique token (UUID), not just 1 or &quot;locked&quot;&lt;br&gt;
The atomicity of SET NX is the core guarantee. There is no gap between &quot;check if key exists&quot; and &quot;set the key&quot; — it's one operation. Two requests racing to acquire the same lock cannot both succeed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Retry logic&lt;/strong&gt;&lt;br&gt;
Rather than immediately rejecting a request when the lock is held, the implementation retries up to 50 times with a 100ms delay between each attempt - giving it up to 5 seconds to acquire the lock before giving up. This handles brief overlaps gracefully without dropping legitimate requests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The TTL&lt;/strong&gt;&lt;br&gt;
The 10-second expiry is a safety net. If the application crashes mid-operation and fails to release the lock, it will expire automatically. Without this, one crash would lock that user's wallet forever.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Lua script on release&lt;/strong&gt;&lt;br&gt;
This is where most implementations get it wrong. A naive release looks like this:&lt;/p&gt;
&lt;p&gt;await redisClient.del(key); // ❌ dangerous&lt;br&gt;
Here's why that's broken: imagine a slow operation acquires the lock, but takes longer than 10 seconds. The TTL expires. A second operation acquires the lock. Now the first operation finishes and calls del - it just released the second operation's lock, not its own. The guarantee is broken.&lt;/p&gt;
&lt;p&gt;The Lua script fixes this by checking the token before deleting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lua&quot;&gt;if redis.call(&quot;GET&quot;, KEYS[1]) == ARGV[1] then
  return redis.call(&quot;DEL&quot;, KEYS[1])
else
  return 0
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because Lua scripts execute atomically in Redis, the check-and-delete is a single uninterruptible operation. You can only release a lock you actually own.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The withLock wrapper&lt;/strong&gt;&lt;br&gt;
Rather than exposing raw acquireLock / releaseLock functions and trusting every caller to use them correctly, the implementation wraps the entire pattern in a higher-order function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/services/wallet.service.ts
export const debitWallet = async (userId: string, amount: number): Promise&amp;lt;Transaction&amp;gt; =&amp;gt; {
  return await withLock(userId, async () =&amp;gt; {
    const wallet = await prisma.wallet.findUnique({ where: { userId } });

    if (!wallet || wallet.balance.lessThan(amount)) {
      throw new Error(&quot;Insufficient balance&quot;);
    }

    // create transaction, update balance...
    return transaction;
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The finally block inside withLock ensures the lock is always released, even if the operation throws. A lock that leaks on error is as bad as no lock at all.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Preventing Deadlocks on Transfers&lt;/strong&gt;&lt;br&gt;
A transfer touches two wallets - sender and receiver. If two transfers run simultaneously in opposite directions (A→B and B→A), they can deadlock: each holds one lock and waits for the other indefinitely.&lt;/p&gt;
&lt;p&gt;The fix is to always acquire locks in the same order, regardless of who is sending to whom. We sort the user IDs before acquiring:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export const withMultiLock = async &amp;lt;T&amp;gt;(userIds: string[], fn: () =&amp;gt; Promise&amp;lt;T&amp;gt;): Promise&amp;lt;T&amp;gt; =&amp;gt; {
  const sortedIds = [...new Set(userIds)].sort();
  const keys = sortedIds.map((id) =&amp;gt; `wallet:lock:${id}`);
  const tokens: string[] = [];

  try {
    for (const key of keys) {
      let token: string | null = null;
      for (let attempt = 0; attempt &amp;lt;= LOCK_RETRY_COUNT; attempt++) {
        token = await acquireLock(key);
        if (token) break;
        await new Promise((resolve) =&amp;gt; setTimeout(resolve, LOCK_RETRY_DELAY_MS));
      }
      if (!token) throw new Error(&quot;Could not acquire lock — resource is busy&quot;);
      tokens.push(token);
    }
    return await fn();
  } finally {
    await Promise.all(keys.map((key, i) =&amp;gt; releaseLock(key, tokens[i])));
  }
};

// In transfer service:
const result = await withMultiLock([senderId, receiver.id], async () =&amp;gt; {
  // both wallets locked — safe to proceed
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If A→B and B→A both arrive at the same time, both will sort to [A, B] and try to acquire A's lock first. One wins, the other waits. No deadlock.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Full System&lt;/strong&gt;&lt;br&gt;
The distributed lock is one layer in a larger stack. The complete wallet engine includes:&lt;/p&gt;
&lt;p&gt;Idempotency - Idempotency-Key header on credit and debit prevents duplicate transactions on network retries, backed by a 24-hour Redis TTL&lt;br&gt;
Webhook delivery with retry - BullMQ queue, 4 attempts, exponential backoff (2s base), per-attempt audit trail&lt;br&gt;
Balance caching - 60-second Redis TTL, invalidated on every write&lt;br&gt;
Token blacklisting - SHA-256 hashed JWTs stored in Redis with matching expiry, making logout actually secure&lt;br&gt;
Cursor-based pagination - transaction history that scales&lt;br&gt;
Full test suite - 103 tests, all mocked, no database required to run.&lt;br&gt;
Built with TypeScript, Express, Prisma, PostgreSQL, and Redis (ioredis). Deployed on Render with a GitHub Actions CI/CD pipeline.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why This Matters&lt;/strong&gt;&lt;br&gt;
Race conditions in financial systems are not exotic. They're the default outcome when you don't explicitly prevent them. Most wallet tutorials never mention it. Most wallet implementations in production have it.&lt;/p&gt;
&lt;p&gt;The fix isn't complicated - but you have to know to look for it.&lt;/p&gt;
&lt;p&gt;If you're building anything that touches money, check your balance reads. If there's no lock between the read and the write, the race condition is already there.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open for Collaboration&lt;/strong&gt;&lt;br&gt;
This project is open source and actively welcoming contributors. Whether you want to add a feature, improve test coverage, or spot something that could be done better - PRs are open.&lt;/p&gt;
&lt;p&gt;Check out the CONTRIBUTING.md for how to get set up and where to send your changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/tochi27/Wallet_System.git&quot;&gt;https://github.com/tochi27/Wallet_System.git&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live API + Swagger:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://wallet-system-api-16cv.onrender.com/api-docs&quot;&gt;https://wallet-system-api-16cv.onrender.com/api-docs&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20524/how-i-stopped-a-race-condition-from-creating-free-money-in-my-wallet-system</guid>
<pubDate>Sat, 13 Jun 2026 14:56:16 +0000</pubDate>
</item>
<item>
<title>Syncing the Real Market: Consuming Brapi and Feeding Redis with Spring Boot</title>
<link>https://coderlegion.com/17273/syncing-the-real-market-consuming-brapi-and-feeding-redis-with-spring-boot</link>
<description>&lt;p&gt;Hello everyone!&lt;/p&gt;
&lt;p&gt;Continuing the &lt;strong&gt;My Broker B3&lt;/strong&gt; series, today we'll talk about an essential component on the B3 side of our simulator: the &lt;strong&gt;B3 Market Sync API&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Before building the engine that decides whether an order gets executed or rejected, we need to ensure it has access to real market prices. That's exactly what &lt;code&gt;b3-market-sync-api&lt;/code&gt; does: &lt;strong&gt;fetches real asset prices from brapi.dev and stores them in Redis&lt;/strong&gt;, ready for real-time consumption.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-what-is-the-market-sync-&quot;&gt;️ What is the Market Sync?&lt;/h2&gt;
&lt;p&gt;This microservice has a single, well-defined responsibility: to be the &lt;strong&gt;market data producer&lt;/strong&gt; of the ecosystem. It operates completely asynchronously and independently — no other service needs to call it directly.&lt;/p&gt;
&lt;p&gt;The flow is simple and elegant:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[brapi.dev] ◀── Feign Client ── [MarketSyncScheduler]
                                         │
                              market:price:{TICKER}
                                         │
                                      [Redis]
                                         │
                              [B3 Matching Engine] (next post)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;mvp-focus&quot;&gt;MVP Focus&lt;/h2&gt;
&lt;p&gt;The goal here is clear: &lt;strong&gt;have real prices available in Redis with low latency&lt;/strong&gt;. The Matching Engine we'll build in the next post won't query a database or make HTTP calls to get prices — it goes straight to Redis. This ensures the matching decision happens in microseconds.&lt;/p&gt;
&lt;p&gt;In this phase, I prioritized:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scheduled sync with market hours guard&lt;/li&gt;
&lt;li&gt;Redis cache with TTL to prevent stale data&lt;/li&gt;
&lt;li&gt;External API rate limit protection&lt;/li&gt;
&lt;li&gt;Critical bug fixes found during code review&lt;/li&gt;
&lt;li&gt;REST API for querying cached prices&lt;/li&gt;
&lt;li&gt;Swagger documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-tech-stack&quot;&gt;️ Tech Stack&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Technology &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Usage&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Java 21&lt;/strong&gt; + &lt;strong&gt;Spring Boot 3.5.11&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Service core&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Spring Cloud OpenFeign&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Declarative HTTP client for brapi.dev&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Spring Data Redis&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;High-performance cache&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Spring Scheduling&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Periodic synchronization&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Jackson JSR310&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Java 8 Date/Time serialization support&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;SpringDoc OpenAPI&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Swagger UI documentation&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-implementation-pillars&quot;&gt;️ Implementation Pillars&lt;/h2&gt;
&lt;h3 id=&quot;1-feign-client-consuming-brapi&quot;&gt;1. Feign Client — Consuming Brapi&lt;/h3&gt;
&lt;p&gt;Spring Cloud OpenFeign lets you declare an HTTP client as a simple interface, with no boilerplate code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@FeignClient(name = &quot;brapiClient&quot;, url = &quot;${app.brapi.url}&quot;)
public interface BrapiClient {

    @GetMapping(&quot;/quote/{ticker}&quot;)
    BrapiResponseDTO getQuote(
        @PathVariable String ticker,
        @RequestParam String token
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The URL and token are injected via &lt;code&gt;application.yaml&lt;/code&gt;, keeping the code clean and environment-configurable.&lt;/p&gt;
&lt;h3 id=&quot;2-scheduler-with-market-hours-guard&quot;&gt;2. Scheduler with Market Hours Guard&lt;/h3&gt;
&lt;p&gt;The heart of the service is a scheduled job that fires every 30 minutes. But before any API call, it checks whether the market is open:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Scheduled(fixedRateString = &quot;${app.sync.interval:1800000}&quot;)
public void sync() {
    if (!isMarketOpen()) {
        log.info(&quot;Sync aborted: market is closed (outside trading hours or weekend).&quot;);
        return;
    }
    // ... fetch and store prices
}

private boolean isMarketOpen() {
    ZonedDateTime now = ZonedDateTime.now(ZoneId.of(&quot;America/Sao_Paulo&quot;));
    DayOfWeek day = now.getDayOfWeek();
    int hour = now.getHour();

    if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) {
        return false;
    }

    // B3 trading hours: 10:00 to 18:00 (Sao Paulo time)
    return hour &amp;gt;= 10 &amp;amp;&amp;amp; hour &amp;lt; 18;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important detail:&lt;/strong&gt; we use &lt;code&gt;ZonedDateTime&lt;/code&gt; with &lt;code&gt;America/Sao_Paulo&lt;/code&gt; explicitly. Inside a Docker container, the JVM may default to UTC — which would make the market hours guard behave incorrectly. We also pass &lt;code&gt;-Duser.timezone=America/Sao_Paulo&lt;/code&gt; in the Dockerfile &lt;code&gt;ENTRYPOINT&lt;/code&gt; to ensure the JVM honors the correct timezone:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;ENTRYPOINT [&quot;java&quot;, &quot;-Duser.timezone=America/Sao_Paulo&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-redis-cache-with-correct-ttl&quot;&gt;3. Redis Cache with Correct TTL&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;MarketDataService&lt;/code&gt; stores each quote in Redis with a standardized key and 5-minute TTL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private static final Duration CACHE_TTL = Duration.ofMinutes(5);

public void saveToCache(BrapiResultDTO quote) {
    String key = CACHE_KEY_PREFIX + quote.getTicker();
    // TTL of 5 minutes — if sync stops, stale prices expire quickly
    // preventing the Matching Engine from using outdated data
    redisTemplate.opsForValue().set(key, quote, CACHE_TTL);
    log.info(&quot;Ticker {} updated in cache: R$ {}&quot;, quote.getTicker(), quote.getRegularMarketPrice());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The TTL is fundamental: if the service goes down, prices expire in 5 minutes. The Matching Engine then fails to find the price in Redis and rejects orders — correct and predictable behavior, much better than using hours-old prices.&lt;/p&gt;
&lt;h3 id=&quot;4-epoch-timestamp-deserialization&quot;&gt;4. Epoch Timestamp Deserialization&lt;/h3&gt;
&lt;p&gt;A subtle bug I found: brapi.dev returns the &lt;code&gt;regularMarketTime&lt;/code&gt; field as a &lt;strong&gt;Unix epoch&lt;/strong&gt; (integer), but Jackson was trying to deserialize it as &lt;code&gt;LocalDateTime&lt;/code&gt; (ISO string). This causes silent deserialization failure.&lt;/p&gt;
&lt;p&gt;The solution was a custom deserializer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class EpochToLocalDateTimeDeserializer extends StdDeserializer&amp;lt;LocalDateTime&amp;gt; {

    private static final ZoneId SAO_PAULO = ZoneId.of(&quot;America/Sao_Paulo&quot;);

    @Override
    public LocalDateTime deserialize(JsonParser parser, DeserializationContext context)
            throws IOException {
        long epoch = parser.getLongValue();
        return LocalDateTime.ofInstant(Instant.ofEpochSecond(epoch), SAO_PAULO);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And annotating it on the DTO:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@JsonDeserialize(using = EpochToLocalDateTimeDeserializer.class)
private LocalDateTime regularMarketTime;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5-rate-limit-protection&quot;&gt;5. Rate Limit Protection&lt;/h3&gt;
&lt;p&gt;Since we use the brapi.dev free plan, we added a delay between calls to avoid throttling:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;for (String ticker : brapiProperties.getTickers()) {
    try {
        BrapiResponseDTO response = brapiClient.getQuote(ticker.trim(), brapiProperties.getToken());
        if (nonNull(response.getResults()) &amp;amp;&amp;amp; !response.getResults().isEmpty()) {
            marketDataService.saveToCache(response.getResults().getFirst());
        }
        // Small delay between calls to avoid hitting rate limit on free plan
        Thread.sleep(200);
    } catch (Exception e) {
        log.error(&quot;Failed to sync ticker {}: {}&quot;, ticker, e.getMessage());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The per-ticker &lt;code&gt;try/catch&lt;/code&gt; is essential — if one asset fails, the loop continues for the rest.&lt;/p&gt;
&lt;h3 id=&quot;6-rest-api-swagger&quot;&gt;6. REST API + Swagger&lt;/h3&gt;
&lt;p&gt;Although the Matching Engine consumes prices directly from Redis, we added REST endpoints to make debugging and observability easier:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Method &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Endpoint &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;GET &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/quotes&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;List prices for all configured tickers&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;GET &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/quotes/{ticker}&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Get price for a specific ticker&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Documented via Swagger UI at &lt;code&gt;http://localhost:8096/swagger-ui.html&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;security-token-out-of-code&quot;&gt;Security — Token Out of Code&lt;/h2&gt;
&lt;p&gt;A critical issue I fixed: the brapi.dev token was hardcoded as a default value in &lt;code&gt;application.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# ❌ Before — token exposed in public repository
token: ${BRAPI_TOKEN:mFk9YRLijAtxsP5y1XxxxX}

# ✅ After — no fallback, variable is required
token: ${BRAPI_TOKEN}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Never expose credentials as default values in public repositories. The application now fails on startup if the variable is not provided — correct behavior.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-validating-the-execution&quot;&gt;✅ Validating the Execution&lt;/h2&gt;
&lt;p&gt;With the application running locally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Tomcat started on port &lt;code&gt;8096&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;✅ Application initialized in under 3 seconds&lt;/li&gt;
&lt;li&gt;✅ Scheduler fired and logged: &lt;code&gt;Sync aborted: market is closed (outside trading hours or weekend)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;✅ Swagger UI accessible at &lt;code&gt;http://localhost:8096/swagger-ui.html&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-s-next-&quot;&gt;What's Next?&lt;/h2&gt;
&lt;p&gt;With &lt;code&gt;b3-market-sync-api&lt;/code&gt; up and feeding Redis with real prices, we have the foundation the next service needs to work. In the next post we'll build the &lt;strong&gt;B3 Matching Engine&lt;/strong&gt; — the engine that will consume these prices and decide whether the broker's orders are executed or rejected.&lt;/p&gt;
&lt;p&gt;Got any questions about the Redis integration or the market hours guard? Drop them in the comments!&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;about-the-series&quot;&gt;About the Series&lt;/h2&gt;
&lt;p&gt;⬅️ Previous Post: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/rvneto/from-stream-to-database-processing-market-data-with-spring-boot-redis-and-flyway-35m9&quot;&gt;From Stream to Database: Processing Market Data with Spring Boot, Redis and Flyway&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;➡️ Next Post: The Heart of B3: Building the Matching Engine with RabbitMQ and Redis &lt;em&gt;(coming soon)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt; Series Index: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/rvneto/series-roadmap-building-a-stock-brokerage-simulator-with-microservices-kgh&quot;&gt;Series Roadmap&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/roberto-de-vargas/&quot;&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rvneto/trading-b3-market-sync&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17273/syncing-the-real-market-consuming-brapi-and-feeding-redis-with-spring-boot</guid>
<pubDate>Sun, 10 May 2026 11:03:34 +0000</pubDate>
</item>
<item>
<title>Redis Hacks Your Future Self Will Thank You For  (No More Crashes)</title>
<link>https://coderlegion.com/17110/redis-hacks-your-future-self-will-thank-you-for-no-more-crashes</link>
<description>&lt;p&gt;Redis isn't just a fancy cache - it's a grenade. Pull the wrong pin (ahem, maxmemory), and BOOM &lt;br&gt;
maxmemory &amp;amp; Eviction Policies: Stop Redis From Ghosting Your Data&lt;/p&gt;
&lt;p&gt;The Problem: Redis doesn't magically clean up. Set maxmemory too high, and it'll eat your RAM like a Cookie Monster. Set it too low, and it'll evict data mid-transaction.&lt;/p&gt;
&lt;p&gt;The Fix:&lt;/p&gt;
&lt;p&gt;First:set the max memory limit that Redis can't exceed out of your server resources&lt;br&gt;
Use the eviction strategy option that meets your needs. Example:&lt;br&gt;
volatile-lru for mixed workloads (bye-bye, least-recently-used expired keys).&lt;br&gt;
allkeys-lfu if you're a data hoarder (prioritize frequently used keys, even if they're immortal).&lt;/p&gt;
&lt;p&gt;All options:&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=1928995982512387623&quot; alt=&quot;&quot;&gt;&lt;br&gt;
EvictionsPro Tip: Test evictions before your app trends on Hacker News.&lt;/p&gt;
&lt;p&gt;Lua Scripts: Your Atomic Weapon Against Race Conditions&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=7197355793863260794&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Why It Matters:&lt;br&gt;
5 clients updating the same key? Chaos. Lua scripts execute atomically.&lt;br&gt;
LuaLater, you can pull your data from Redis into whatever DB you use for consistency.&lt;br&gt;
Retries, Timeouts &amp;amp; Processors (Yes, That's a Word Now)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Retries: 3 retries with exponential backoff. More? You're just DDoSing yourself.&lt;/li&gt;
&lt;li&gt;Timeouts: connect_timeout: 5s, read_timeout: 2s. Your app isn't a hostage negotiator.&lt;/li&gt;
&lt;li&gt;Processors: Redis is single-threaded. If your CPU cores are screaming, shard or migrate heavy ops to background workers.&lt;br&gt;
Redis isn't 'set it and forget it' - it's 'set it, tweak it, and make your on-call rotations boring again'. Try one tip today, and thank me when you're sleeping through deploy windows.&lt;br&gt;
Comment your worst Redis horror story - bonus points for plot twists involving FLUSHALL&lt;br&gt;
Thanks for reading&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17110/redis-hacks-your-future-self-will-thank-you-for-no-more-crashes</guid>
<pubDate>Fri, 08 May 2026 20:52:18 +0000</pubDate>
</item>
<item>
<title>Why Your &quot;Fail-Fast&quot; Strategy is Killing Your Distributed System (and How to Fix It)</title>
<link>https://coderlegion.com/16782/why-your-fail-fast-strategy-is-killing-your-distributed-system-and-how-to-fix-it</link>
<description>&lt;p&gt;It's 2 AM. PagerDuty fires. Redis master is down. Your application, trained to fail fast, dutifully fails — every single request, all at once. By the time Sentinel promotes a new master 12 seconds later, you've already generated 40,000 errors and three escalation calls. The system recovered on its own. Your application didn't let it.&lt;/p&gt;
&lt;p&gt;This is the story of how &quot;good engineering&quot; can make a 12-second infrastructure event into a 12-minute outage — and how to design boundaries that prevent it.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; — During infrastructure failovers (Redis, Kafka, etcd), blind fail-fast amplifies instability. Bounded retry — centralized, time-boxed, invisible to business logic — absorbs the 10–15 second recovery window without leaking infrastructure noise to users. Resilience is not a library. It is a contract between layers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-core-question&quot;&gt;The Core Question&lt;/h3&gt;
&lt;p&gt;When your session storage — Redis, Memcached, or any stateful dependency — goes temporarily unavailable, you face a fundamental architectural choice:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Should you fail fast? Or should you retry?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We all learned fail-fast as gospel. And it is — until it isn't. During transient infrastructure events like leader elections, blind fail-fast propagates instability instead of containing it. The response you choose determines whether the incident resolves itself in 12 seconds or snowballs into a 12-minute outage with three bridge calls.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-actually-happens-during-failover&quot;&gt;What Actually Happens During Failover&lt;/h3&gt;
&lt;p&gt;To understand why fail-fast can backfire, look at the mechanics of a Redis Sentinel failover:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Phase &lt;/th&gt;&lt;th&gt;Duration &lt;/th&gt;&lt;th&gt;What Happens&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Detection&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;~10–12s &lt;/td&gt;&lt;td&gt;Sentinel quorum detects master is down&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Election&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;~1–2s &lt;/td&gt;&lt;td&gt;Sentinels agree on a new master&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Promotion&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;~1s &lt;/td&gt;&lt;td&gt;Replica promoted, clients notified&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Reconnection&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;~1–3s &lt;/td&gt;&lt;td&gt;Clients re-establish connections&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;Note: these phases overlap. Total failover typically completes in 12–15 seconds, not the sum of individual phases. Reconnection time also depends heavily on your client library — a Sentinel-aware client with topology refresh (e.g., Lettuce, go-redis with Sentinel support) reconnects in under a second, while a naive connection pool can take 30s+.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;During this window, your application sees TCP dial timeouts and connection resets. Nothing is broken. No data is lost. The system is doing exactly what it was designed to do — electing a new leader. Your application just needs to not panic for 12 seconds.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-blind-fail-fast-is-dangerous&quot;&gt;Why Blind Fail-Fast Is Dangerous&lt;/h3&gt;
&lt;p&gt;If your application fails immediately on the first connection timeout during this window, four things happen in rapid succession:&lt;/p&gt;
&lt;h4 id=&quot;1-instability-amplification&quot;&gt;1. Instability Amplification&lt;/h4&gt;
&lt;p&gt;A 3-second infrastructure blip becomes a user-visible outage. Every request during the failover window returns an error, even though the system would have recovered on its own.&lt;/p&gt;
&lt;h4 id=&quot;2-infrastructure-semantics-leak-upward&quot;&gt;2. Infrastructure Semantics Leak Upward&lt;/h4&gt;
&lt;p&gt;Your business layer now exposes raw infrastructure details — &quot;Redis connection refused&quot; — to clients that have no idea what Redis is or why it matters.&lt;/p&gt;
&lt;h4 id=&quot;3-uncontrolled-client-retries&quot;&gt;3. Uncontrolled Client Retries&lt;/h4&gt;
&lt;p&gt;Clients receiving errors start retrying independently. If you have 1,000 concurrent users and each retries 3 times, you just turned 1,000 QPS into 3,000 QPS — hitting an infrastructure layer that's already struggling to stabilize.&lt;/p&gt;
&lt;h4 id=&quot;4-retry-storms&quot;&gt;4. Retry Storms&lt;/h4&gt;
&lt;p&gt;This is the catastrophic outcome. Unbounded retries create cascading load amplification. CPU spikes prevent recovery. The system enters an instability feedback loop where the act of trying to recover keeps the system down. I've seen retry storms take down entire regions.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;&quot;Your timeout config was technically correct. Your system was functionally down. That's not a timeout problem — that's a design problem.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's the distinction that actually matters in production: &lt;strong&gt;the failure TYPE must determine your recovery strategy.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Infrastructure-Level &lt;/th&gt;&lt;th&gt;Business-Level&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Examples&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Network jitter, leader election, connection reset, &lt;code&gt;READONLY&lt;/code&gt; replica response &lt;/td&gt;&lt;td&gt;Validation error, permission denial, domain rule violation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Nature&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Transient — will resolve on its own &lt;/td&gt;&lt;td&gt;Permanent — retrying won't help&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Strategy&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;ABSORB&lt;/strong&gt; — retry within bounds &lt;/td&gt;&lt;td&gt;&lt;strong&gt;FAIL FAST&lt;/strong&gt; — return error immediately&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Treating a leader election timeout the same as a schema validation error is an architectural mistake. One will resolve in seconds; the other will never succeed no matter how many times you retry.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-failure-boundary-model&quot;&gt;The Failure Boundary Model&lt;/h3&gt;
&lt;p&gt;This is the architectural pattern that makes everything work:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mermaid.ink/img/Z3JhcGggVEQKICAgIHN1YmdyYXBoIENsaWVudF9MYXllciBbQ2xpZW50IExheWVyXQogICAgICAgIENbUmV0cmllcyBvbmx5IHdoZW4gc2lnbmFsZWQgcmV0cnlhYmxlXQogICAgZW5kCgogICAgc3ViZ3JhcGggQnVzaW5lc3NfTGF5ZXIgW0J1c2luZXNzIExheWVyXQogICAgICAgIEJbUHJlc2VydmVzIHNlbWFudGljIGludGVncml0eTxici8-RkFJTC1GQVNUIEJPVU5EQVJZXQogICAgZW5kCgogICAgc3ViZ3JhcGggSW5mcmFzdHJ1Y3R1cmVfQm91bmRhcnkgW0luZnJhc3RydWN0dXJlIEJvdW5kYXJ5XQogICAgICAgIElbQWJzb3JicyB0cmFuc2llbnQgaW5zdGFiaWxpdHk8YnIvPlJFVFJZIEJPVU5EQVJZXQogICAgZW5kCgogICAgc3ViZ3JhcGggRGVwZW5kZW5jeSBbRGVwZW5kZW5jeV0KICAgICAgICBEW1JlZGlzIC8gTkFUUyAvIEthZmthIC8gREJdCiAgICBlbmQKCiAgICBDIC0tPiBCCiAgICBCIC0tICJCdXNpbmVzcyBFcnJvcnM6IEZhaWwgSW1tZWRpYXRlbHkiIC0tPiBDCiAgICBCIC0tPiBJCiAgICBJIC0tICJCb3VuZGVkIFJldHJ5OiBBYnNvcmJzIDEwLTE1cyBub2lzZSIgLS0-IEIKICAgIEkgLS0-IEQ=&quot; alt=&quot;graph TD&quot;&gt;&lt;/p&gt;
&lt;p&gt;The retry boundary sits in the &lt;strong&gt;infrastructure client wrapper&lt;/strong&gt; — the thin layer between your business code and the dependency client. Not in HTTP middleware, not in individual service handlers, not in a sidecar. In the client wrapper itself.&lt;/p&gt;
&lt;p&gt;Why does this matter? Because if retry logic exists at multiple layers, you get retry amplification. I've seen teams with retry in the HTTP handler, the service layer, AND the Redis client — producing 3 × 3 × 3 = 27 attempts per original request. That's not resilience. That's a DDoS against your own infrastructure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key principles:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Retry belongs at the infrastructure boundary&lt;/strong&gt; — one place, one policy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Business logic must remain fail-fast&lt;/strong&gt; — semantic errors should never be retried.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;By the time an error reaches the client, it has been vetted and classified.&lt;/strong&gt; We are designing for predictability.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;bounded-retry-implementation&quot;&gt;Bounded Retry: Implementation&lt;/h3&gt;
&lt;p&gt;If we're going to retry, we must do it with discipline. Four pillars:&lt;/p&gt;
&lt;h4 id=&quot;1-centralized&quot;&gt;1. Centralized&lt;/h4&gt;
&lt;p&gt;Retry logic lives in one place — the infrastructure client wrapper. Not in individual handlers, not in middleware, not in the business layer. One retry boundary per dependency, one policy, one set of metrics.&lt;/p&gt;
&lt;h4 id=&quot;2-time-bounded&quot;&gt;2. Time-Bounded&lt;/h4&gt;
&lt;p&gt;We define a &lt;strong&gt;retry budget&lt;/strong&gt; — for example, 15 seconds. Why 15? Because it encapsulates the 10–12 second Sentinel detection window plus a margin for stabilization and reconnection. Time-based budgets are superior to pure attempt counts because they normalize across different failure modes — a retry that takes 5s per attempt behaves very differently from one that takes 100ms.&lt;/p&gt;
&lt;h4 id=&quot;3-attempt-limited-with-jitter&quot;&gt;3. Attempt-Limited with Jitter&lt;/h4&gt;
&lt;p&gt;Maximum 2–3 retry attempts within the budget window, with exponential backoff and &lt;strong&gt;jitter&lt;/strong&gt;. Without jitter, synchronized retries from multiple application instances create a thundering herd — everyone hits the new master at exactly the same moment.&lt;/p&gt;
&lt;h4 id=&quot;4-invisible-to-business-logic&quot;&gt;4. Invisible to Business Logic&lt;/h4&gt;
&lt;p&gt;If the retry succeeds within the budget, the business layer never knew there was a problem. If it fails, the business layer receives a clean, classified error — not a raw TCP stack trace that means nothing to anyone above the infrastructure layer.&lt;/p&gt;
&lt;p&gt;Here's what this looks like in practice:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Bounded retry wrapper — lives in the infrastructure client layer
func withBoundedRetry(ctx context.Context, budget time.Duration, maxAttempts int, op func() error) error {
    deadline := time.Now().Add(budget)
    var lastErr error

    for attempt := 0; attempt &amp;lt; maxAttempts; attempt++ {
        if time.Now().After(deadline) {
            break
        }

        lastErr = op()
        if lastErr == nil {
            return nil // success — business layer never knew
        }

        if !isRetryable(lastErr) {
            return normalizeError(lastErr) // permanent failure — fail fast
        }

        // Exponential backoff with jitter
        backoff := time.Duration(1&amp;lt;&amp;lt;attempt) * 500 * time.Millisecond
        jitter := time.Duration(rand.Int63n(int64(backoff / 2)))
        select {
        case &amp;lt;-time.After(backoff + jitter):
        case &amp;lt;-ctx.Done():
            return ctx.Err()
        }
    }

    return normalizeError(lastErr) // budget exhausted — fail deterministically
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;┌─────────────────────────────────────────────┐
│          Retry Budget: 15 seconds           │
│                                             │
│  Attempt 1  →  timeout (5s)  →  backoff     │
│  Attempt 2  →  timeout (5s)  →  backoff     │
│  Attempt 3  →  success                      │
│                                             │
│  Total elapsed: ~11s                        │
│  Application impact: ZERO                   │
│                                             │
│  ─── OR ───                                 │
│                                             │
│  Budget exhausted → FAIL DETERMINISTICALLY  │
│  Clean, classified error to business layer  │
└─────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;&quot;Retry is not infinite. Retry is time-boxed. Once the budget is exhausted, we fail deterministically.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;error-normalization&quot;&gt;Error Normalization&lt;/h3&gt;
&lt;p&gt;This is where most teams get it wrong. They retry everything — or nothing. The retry decision must be driven by error classification:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Raw Error &lt;/th&gt;&lt;th&gt;Normalized To &lt;/th&gt;&lt;th&gt;Retryable? &lt;/th&gt;&lt;th&gt;Why&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;TCP dial timeout&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;UNAVAILABLE&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Connection not established, may recover&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Connection reset&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;UNAVAILABLE&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Transient network disruption&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;READONLY&lt;/code&gt; (replica) &lt;/td&gt;&lt;td&gt;&lt;code&gt;UNAVAILABLE&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Sentinel failover in progress — replica not yet promoted&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Leader election in progress&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;UNAVAILABLE&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Raft/consensus transition&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;OOM command not allowed&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;RESOURCE_EXHAUSTED&lt;/code&gt; &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Backpressure — retrying makes it worse&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;WRONGTYPE&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;INVALID_ARGUMENT&lt;/code&gt; &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Schema error — will never succeed&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;NOPERM&lt;/code&gt; / &lt;code&gt;Permission denied&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;PERMISSION_DENIED&lt;/code&gt; &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Auth failure — will never succeed&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;NOT_FOUND&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;NOT_FOUND&lt;/code&gt; &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Semantic absence — retry won't create the resource&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &lt;code&gt;READONLY&lt;/code&gt; case deserves special attention. During Sentinel failover, a replica that hasn't been promoted yet responds with &lt;code&gt;READONLY&lt;/code&gt; to write commands. If your retry layer treats this as a permanent error, your circuit breaker trips, clients get errors, and a 12-second failover becomes a 5-minute outage while someone manually resets the breaker. Classify &lt;code&gt;READONLY&lt;/code&gt; as &lt;code&gt;UNAVAILABLE&lt;/code&gt; — it will resolve when the new master is promoted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The rule is simple:&lt;/strong&gt; you cannot leak internal implementation details up the stack. Your retry layer must inspect and reclassify errors — not just map them 1:1. Error semantics must align across every layer.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-relationship-with-circuit-breakers&quot;&gt;The Relationship with Circuit Breakers&lt;/h3&gt;
&lt;p&gt;Bounded retry is the &lt;strong&gt;inner loop&lt;/strong&gt; — it handles transient failures within a known recovery window. But what if the dependency is truly down, not just transitioning?&lt;/p&gt;
&lt;p&gt;That's where &lt;strong&gt;circuit breakers&lt;/strong&gt; serve as the &lt;strong&gt;outer loop&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mermaid.ink/img/Z3JhcGggTFIKICAgIFJlcSgoUmVxdWVzdCkpIC0tPiBDQntDaXJjdWl0IEJyZWFrZXI8YnIvPidPdXRlciBMb29wJ30KICAgIENCIC0tICJIZWFsdGh5IiAtLT4gQlJbQm91bmRlZCBSZXRyeTxici8-J0lubmVyIExvb3AnXQogICAgQlIgLS0-IERlcFsoRGVwZW5kZW5jeSldCgogICAgQ0IgLS0gIk9wZW46IEZhaWx1cmUgUmF0ZSBIaWdoIiAtLT4gRkZbRmFzdCBGYWlsXQogICAgQlIgLS0gIkJ1ZGdldCBFeGhhdXN0ZWQiIC0tPiBFcnJbTm9ybWFsaXplZCBFcnJvcl0KCiAgICBzdHlsZSBDQiBmaWxsOiNmOWYsc3Ryb2tlOiMzMzMsc3Ryb2tlLXdpZHRoOjJweAogICAgc3R5bGUgQlIgZmlsbDojYmJmLHN0cm9rZTojMzMzLHN0cm9rZS13aWR0aDoycHg=&quot; alt=&quot;graph LR&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bounded retry&lt;/strong&gt; absorbs transient events (leader election, network jitter) — seconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Circuit breaker&lt;/strong&gt; protects against sustained outages (dependency truly dead) — minutes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without a circuit breaker, sustained failures chew through retry budgets on every request, wasting resources. Without bounded retry, every transient blip trips the circuit breaker unnecessarily. They are complementary, not redundant.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;observability-instrument-the-boundary&quot;&gt;Observability: Instrument the Boundary&lt;/h3&gt;
&lt;p&gt;A production retry boundary must emit metrics. Without them, you're flying blind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;retry_attempt_total&lt;/code&gt;&lt;/strong&gt; — how often retries fire (by dependency, by error type)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;retry_budget_exhausted_total&lt;/code&gt;&lt;/strong&gt; — how often the full budget is consumed without success&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;retry_success_on_attempt&lt;/code&gt;&lt;/strong&gt; — which attempt number succeeds (histogram)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;error_classification&lt;/code&gt;&lt;/strong&gt; — distribution of retryable vs non-retryable errors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The key alert:&lt;/strong&gt; if retry budget exhaustion rate exceeds ~5%, either your budget is too tight or your dependency is degraded beyond transient. This is the signal that distinguishes a leader election from a real outage — and it's the signal that should trigger your circuit breaker.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;beyond-redis-a-universal-pattern&quot;&gt;Beyond Redis: A Universal Pattern&lt;/h3&gt;
&lt;p&gt;If this looks Redis-specific, zoom out. The bounded retry pattern applies to any stateful dependency with leader election:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Redis Sentinel&lt;/strong&gt; — master failover with quorum detection, 10–15s window&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NATS JetStream&lt;/strong&gt; — stream leader election in the Raft group, typically 2–5s with default election timeout&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;etcd / Consul&lt;/strong&gt; — Raft leader election, ~1–2s with default settings, but watch streams may buffer longer&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kafka&lt;/strong&gt; — partition leader election via controller, typically 5–15s depending on &lt;code&gt;replica.lag.time.max.ms&lt;/code&gt; and ISR size&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CockroachDB / TiKV&lt;/strong&gt; — range leader election, similar Raft mechanics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The mechanics are the same everywhere: a detection window, a brief period of unavailability, and then recovery. Design your retry budget to absorb that window. Calibrate the budget to the specific system — 15s for Redis Sentinel, 5s for NATS, 20s for Kafka.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-cross-layer-contract&quot;&gt;The Cross-Layer Contract&lt;/h3&gt;
&lt;p&gt;Resilience is not a library you &lt;code&gt;import&lt;/code&gt;. It is a &lt;strong&gt;contract between layers&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Layer &lt;/th&gt;&lt;th&gt;Responsibility&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Absorbs transient instability via bounded retry&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Business&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Remains fail-fast for semantic integrity&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Client&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Retries only when signaled retryable&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;When failure is bounded and classified, the system becomes &lt;strong&gt;predictable&lt;/strong&gt;. And predictability is the foundation of operational confidence.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;resilience-checklist&quot;&gt;Resilience Checklist&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Retry Budget:&lt;/strong&gt; Is my retry window matched to the dependency's failover time (e.g., 15s for Redis)?&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Jitter:&lt;/strong&gt; Do my retries have randomized sleep to avoid the &quot;Thundering Herd&quot;?&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Error Classification:&lt;/strong&gt; Does my code distinguish between &lt;code&gt;READONLY&lt;/code&gt; (retryable) and &lt;code&gt;PERMISSION_DENIED&lt;/code&gt; (not retryable)?&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Centralization:&lt;/strong&gt; Is my retry logic in the client wrapper, not leaked across handlers?&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Observability:&lt;/strong&gt; Do I have an alert if &quot;Retry Budget Exhausted&quot; exceeds 5%?&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-takeaways-1&quot;&gt;Key Takeaways&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fail fast — but not during transient infrastructure events.&lt;/strong&gt; A leader election is not a business error. Don't treat it like one.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retry must be bounded.&lt;/strong&gt; Time-boxed, attempt-limited, with jitter. No open-ended retry loops.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retry must be centralized.&lt;/strong&gt; One retry boundary per dependency, at the infrastructure layer. Retry in multiple layers = retry amplification.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Failure semantics must be normalized.&lt;/strong&gt; Retryable vs non-retryable must be explicit. Watch for &lt;code&gt;READONLY&lt;/code&gt; — the most common Sentinel failover gotcha.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Resilience requires cross-layer alignment.&lt;/strong&gt; Bounded retry (inner loop) + circuit breaker (outer loop) + observability = production-grade resilience.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;should-distributed-systems-always-fail-fast-&quot;&gt;Should distributed systems always fail fast?&lt;/h4&gt;
&lt;p&gt;No. Fail fast for &lt;strong&gt;business-level errors&lt;/strong&gt; (validation, permission, domain rules), but use &lt;strong&gt;bounded retry&lt;/strong&gt; for transient infrastructure failures like leader election and temporary network instability.&lt;/p&gt;
&lt;h4 id=&quot;what-is-a-reasonable-retry-budget-for-redis-sentinel-failover-&quot;&gt;What is a reasonable retry budget for Redis Sentinel failover?&lt;/h4&gt;
&lt;p&gt;In many production setups, &lt;strong&gt;12-15 seconds&lt;/strong&gt; is a practical starting point because it usually covers Sentinel detection, promotion, and client reconnection. Calibrate with your own failover timings and SLOs.&lt;/p&gt;
&lt;h4 id=&quot;if-the-service-already-retries-should-the-client-also-retry-&quot;&gt;If the service already retries, should the client also retry?&lt;/h4&gt;
&lt;p&gt;Only when explicitly signaled retryable. Blind retries at both layers often create retry amplification and can trigger a retry storm.&lt;/p&gt;
&lt;h4 id=&quot;how-is-bounded-retry-different-from-a-circuit-breaker-&quot;&gt;How is bounded retry different from a circuit breaker?&lt;/h4&gt;
&lt;p&gt;Bounded retry handles short transient windows (inner loop). Circuit breaker handles sustained dependency failure and stops repeated expensive attempts (outer loop).&lt;/p&gt;
&lt;h4 id=&quot;why-not-use-a-service-mesh-istio-for-retries-&quot;&gt;Why not use a Service Mesh (Istio) for retries?&lt;/h4&gt;
&lt;p&gt;While Mesh can retry, the application layer has better &quot;semantic awareness.&quot; Only the app knows if a specific error is safe to retry based on idempotency.&lt;/p&gt;
&lt;h4 id=&quot;when-should-i-not-use-bounded-retry-&quot;&gt;When should I NOT use Bounded Retry?&lt;/h4&gt;
&lt;p&gt;For non-idempotent operations unless you have a robust request-ID tracking system. For business errors (400s), always fail fast.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;further-reading&quot;&gt;Further Reading&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://harrisonsec.com/blog/rust-vs-c-assembly-performance-safety-analysis/&quot;&gt;Rust vs C Assembly: Complete Performance and Safety Analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://harrisonsec.com/blog/legacy-compatibility-lab-full-stack/&quot;&gt;Legacy Compatibility Lab: My Full Stack for Reviving Dead Software&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thought&quot;&gt;Final Thought&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;Distributed systems are not about avoiding failure.&lt;br&gt;
They are about &lt;strong&gt;designing boundaries&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If retry is everywhere, the system becomes unpredictable.&lt;br&gt;
If retry is nowhere, transient instability leaks upward.&lt;/p&gt;
&lt;p&gt;The goal is not infinite retry.&lt;br&gt;
&lt;strong&gt;The goal is bounded retry.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That boundary is what keeps systems stable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Resilience is not a library. It is a contract between layers.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Based on a talk I gave on failure boundary design in distributed systems.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://harrisonsec.com/blog/fail-fast-bounded-resilience-distributed-systems/&quot;&gt;harrisonsec.com&lt;/a&gt;. Listen to the &lt;a rel=&quot;nofollow&quot; href=&quot;https://harrisonsec.com/audio/why-failing-fast-triggers-cascading-failures.m4a&quot;&gt;deep dive audio&lt;/a&gt; for a detailed walkthrough.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16782/why-your-fail-fast-strategy-is-killing-your-distributed-system-and-how-to-fix-it</guid>
<pubDate>Tue, 05 May 2026 19:11:27 +0000</pubDate>
</item>
<item>
<title>✨ A New Stack — Introducing Redis Nuxt Blog: A Sci-Fi Blog Boilerplate from a Galaxy Not So Far Away</title>
<link>https://coderlegion.com/15670/new-stack-introducing-redis-nuxt-blog-sci-fi-blog-boilerplate-from-a-galaxy-not-so-far-away</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;“A long time ago, in a server rack far, far away, a lone developer sought a faster way to publish their thoughts across the galaxy…”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you’re a developer roaming the cosmic void in search of the perfect blogging platform — one fast enough to outrun hyperspace lag, simple enough to configure from a single holocron file, and powered by the mighty Force of Redis — your quest ends here.&lt;/p&gt;
&lt;p&gt;Welcome to &lt;strong&gt;Redis Nuxt Blog&lt;/strong&gt;, a futuristic, high-performance boilerplate forged with &lt;strong&gt;Nuxt 3&lt;/strong&gt;, &lt;strong&gt;Redis Stack&lt;/strong&gt;, and a containerized fleet of &lt;strong&gt;Docker Star Destroyers&lt;/strong&gt;.&lt;br&gt;
Inspired by the legendary &lt;em&gt;Salvatore “antirez” Sanfilippo&lt;/em&gt;, creator of Redis and intergalactic craftsman of absurdly fast data structures, this project aims to honor that sci-fi spirit — equal parts elegance, speed, and space-wizardry.&lt;/p&gt;
&lt;p&gt;Grab your lightsabers, devs. We’re jumping to lightspeed. ⚡&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/melasistema/redis-blog/master/resources/images/redis-blog-poster-image.jpg&quot; alt=&quot;Redis Nuxt Blog&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-another-blog-boilerplate-&quot;&gt;Why Another Blog Boilerplate?&lt;/h3&gt;
&lt;p&gt;Because most blog starters are like broken droids — sluggish, uncustomizable, and likely to beep something rude at you when you change a font.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Redis Nuxt Blog&lt;/strong&gt; is built for developers who want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A fast, SSR-friendly, SEO-happy front-end&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A database engine so quick it would embarrass a pod-racer&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A deeply configurable UI without fiddling with 27 separate config files&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A ready-to-launch Docker setup&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A CLI so efficient it feels like talking to R2-D2 (but without the panic screams)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A theming system powered by Tailwind CSS, customizable from one file&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Markdown-powered post content&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dynamic excerpts and content width control&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-core-features-translated-from-wookiee-&quot;&gt;✨ Core Features (Translated from Wookiee)&lt;/h3&gt;
&lt;h4 id=&quot;nuxt-js-3-frontend&quot;&gt;Nuxt.js 3 Frontend&lt;/h4&gt;
&lt;p&gt;Modern. Fast. SSR-powered.&lt;br&gt;
Perfect for blogs, portals, and broadcasting your galactic manifestos.&lt;/p&gt;
&lt;h4 id=&quot;-redis-stack-as-the-primary-database&quot;&gt;⚡ Redis Stack as the Primary Database&lt;/h4&gt;
&lt;p&gt;Forget SQL. Forget slow disks.&lt;br&gt;
We harness the &lt;strong&gt;Quantum Hyperdrive&lt;/strong&gt; (a.k.a RedisJSON + Redis Sorted Sets) to store posts with blazing speed.&lt;/p&gt;
&lt;p&gt;Salvatore Sanfilippo didn’t intend Redis to be a sci-fi artifact… but let’s be honest, it might as well have been forged from midichlorians.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/melasistema/redis-blog/master/resources/images/redis-blog-hyperdrive-performance.jpg&quot; alt=&quot;Redis Nuxt Blog&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;one-config-file-to-rule-them-all-now-with-full-tailwind-integration-&quot;&gt;One Config File to Rule Them All (Now With Full Tailwind Integration)&lt;/h4&gt;
&lt;p&gt;Every visual setting — fonts, weights, palette, spacing, even content width — comes from:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;config/blog.config.ts&lt;/code&gt; &lt;/p&gt;
&lt;p&gt;This single file now controls:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Typography per heading level&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Full Tailwind color palette generation&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Global content max-width&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Header title &amp;amp; tagline&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Layout constraints&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Balance to the styling Force, at last.&lt;/p&gt;
&lt;h4 id=&quot;-post-excerpts-new-&quot;&gt;✂️ Post Excerpts (New!)&lt;/h4&gt;
&lt;p&gt;Enable &lt;strong&gt;post previews&lt;/strong&gt; on the homepage with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;postExcerpt.enabled&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;postExcerpt.maxLength&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Perfect for teasing your readers like a Holonet trailer.&lt;/p&gt;
&lt;h4 id=&quot;-post-navigation&quot;&gt;↔️ Post Navigation&lt;/h4&gt;
&lt;p&gt;Navigate between posts like browsing encrypted Jedi archives.&lt;/p&gt;
&lt;h4 id=&quot;markdown-support&quot;&gt;Markdown Support&lt;/h4&gt;
&lt;p&gt;Posts are fully Markdown-powered — headings, lists, code blocks, even multi-line input via CLI.&lt;/p&gt;
&lt;p&gt;Write your galactic knowledge in pure Markdown. No HTML acrobatics needed.&lt;/p&gt;
&lt;h4 id=&quot;dockerized-everything&quot;&gt;Dockerized Everything&lt;/h4&gt;
&lt;p&gt;Spin up Nuxt + Redis with one command:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker compose up -d --build&lt;/code&gt; &lt;/p&gt;
&lt;p&gt;Your dev environment becomes as consistent as Stormtrooper aim (but… good).&lt;/p&gt;
&lt;h4 id=&quot;content-cli-upgraded-&quot;&gt;Content CLI (Upgraded!)&lt;/h4&gt;
&lt;p&gt;Create, list, delete posts with simple commands:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;npm run new-post
npm run post:list
npm run post:delete&lt;/code&gt; &lt;/p&gt;
&lt;p&gt;The CLI now supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Multi-line Markdown input&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Automatically generated slugs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Default author fallback&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You are now the administrator of your own Holocron Library.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-tech-stack-what-our-starship-is-made-of&quot;&gt;️ Tech Stack: What Our Starship Is Made Of&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Nuxt 3&lt;/strong&gt; — the elegant lightsaber&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Redis Stack w/ RedisJSON&lt;/strong&gt; — the hyperdrive core&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tailwind CSS&lt;/strong&gt; — the robe of a modern Jedi developer&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Google Fonts module&lt;/strong&gt; — for typographic wizardry&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Docker Compose&lt;/strong&gt; — the Imperial Fleet&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;TypeScript + tsx runtime&lt;/strong&gt; — because even smugglers need types&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/melasistema/redis-blog/master/resources/images/redis-blog-the-ultimate-tech-stack.jpg&quot; alt=&quot;Redis Nuxt Blog&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-the-holocron-of-configuration-updated-&quot;&gt;️ The Holocron of Configuration (Updated!)&lt;/h3&gt;
&lt;p&gt;Everything — fonts, colors, layout, excerpts, pagination — lives in:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;config/blog.config.ts&lt;/code&gt; &lt;/p&gt;
&lt;p&gt;New additions include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;headerTitle&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;headerTagline&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dynamic Tailwind theme generation&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;contentMaxWidth&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Post excerpts config&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Want your blog to have an imperial blue palette?&lt;br&gt;
Done.&lt;/p&gt;
&lt;p&gt;Want H1 in &lt;em&gt;Englebert&lt;/em&gt; and H2 in &lt;em&gt;Roboto Slab&lt;/em&gt;?&lt;br&gt;
Absolutely.&lt;/p&gt;
&lt;p&gt;Want your content width to feel like the inside of a Star Destroyer corridor?&lt;br&gt;
Just set a new max-width.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;api-endpoints-for-the-droids-among-us-&quot;&gt;API Endpoints (For the Droids Among Us)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Endpoint &lt;/th&gt;&lt;th&gt;Method &lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;/api/posts&lt;/code&gt; &lt;/td&gt;&lt;td&gt;GET &lt;/td&gt;&lt;td&gt;Retrieve posts. Supports &lt;code&gt;?page=N&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;/api/posts/:slug&lt;/code&gt; &lt;/td&gt;&lt;td&gt;GET &lt;/td&gt;&lt;td&gt;Fetch single post + neighbors&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;/api/tags&lt;/code&gt; &lt;/td&gt;&lt;td&gt;GET &lt;/td&gt;&lt;td&gt;Retrieve all tags&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Perfect for custom frontends, mobile apps, or your own droid companion.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-developers-love-this&quot;&gt;Why Developers Love This&lt;/h3&gt;
&lt;p&gt;Because &lt;strong&gt;simplicity&lt;/strong&gt; + &lt;strong&gt;power&lt;/strong&gt; = developer bliss.&lt;/p&gt;
&lt;p&gt;Because Redis is a piece of sci-fi technology disguised as a database.&lt;/p&gt;
&lt;p&gt;Because Nuxt 3 is smooth as sliding through hyperspace.&lt;/p&gt;
&lt;p&gt;Because Tailwind lets you customize everything without rewriting CSS.&lt;/p&gt;
&lt;p&gt;Because Markdown posts feel like writing Jedi scrolls.&lt;/p&gt;
&lt;p&gt;And because every post stored in Redis Sorted Sets is basically organized by the Force.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;author-amp-origin-story&quot;&gt;Author &amp;amp; Origin Story&lt;/h3&gt;
&lt;p&gt;Built by &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/melasistema&quot;&gt;Luca Visciola&lt;/a&gt;&lt;/strong&gt; — drummer, developer, and happy dad.&lt;/p&gt;
&lt;p&gt;Inspired by &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://antirez.com&quot;&gt;Salvatore “antirez” Sanfilippo&lt;/a&gt;&lt;/strong&gt;, whose creation Redis is widely believed to be running not on silicon… but on compressed fragments of a neutron star.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-transmission&quot;&gt;Final Transmission&lt;/h3&gt;
&lt;p&gt;If you're searching for a fast, flexible, sci-fi-friendly blog engine that handles posts faster than the Millennium Falcon outruns TIE Fighters, this boilerplate may be the one you’re looking for.&lt;/p&gt;
&lt;p&gt;Clone it.&lt;br&gt;
Customize it.&lt;br&gt;
Seed it.&lt;br&gt;
Deploy it.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;GitHub Repository:&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/melasistema/redis-blog&quot;&gt;https://github.com/melasistema/redis-blog&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;And may the &lt;strong&gt;Force of Redis&lt;/strong&gt; be with you. ❤️‍&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15670/new-stack-introducing-redis-nuxt-blog-sci-fi-blog-boilerplate-from-a-galaxy-not-so-far-away</guid>
<pubDate>Thu, 23 Apr 2026 16:49:31 +0000</pubDate>
</item>
<item>
<title>Database Interaction: Connecting to Databases</title>
<link>https://coderlegion.com/390/database-interaction-connecting-to-databases</link>
<description>&lt;p&gt;In this article, we will address the key issue of database connections in software development. Large volumes of data are effectively stored and managed via databases, which form the foundation of many applications. Building scalable and reliable apps requires developers to have a solid understanding of database connections.&lt;br&gt;In software development, database connectivity is essential for maintaining &lt;strong&gt;data integrity&lt;/strong&gt;, &lt;strong&gt;scalability&lt;/strong&gt;, and &lt;strong&gt;system performance&lt;/strong&gt;. Relational databases like MySQL and PostgreSQL offer structured storage and sophisticated querying, whereas NoSQL databases like MongoDB and Redis offer flexibility for unstructured data. Comprehending these distinctions is essential for developers to make well-informed decisions.&lt;/p&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Selecting the Right Database&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;This tutorial offers information on different kinds of databases and things to think about.&lt;br&gt;Different Types of Databases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Relational Databases:&lt;/strong&gt; These databases arrange information into structured tables that have pre-established connections between them. PostgreSQL, SQL Server, and MySQL are a few examples.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NoSQL database&lt;/strong&gt;: Large amounts of unstructured or semi-structured data are easily handled by NoSQL databases because they provide a more flexible data format. Redis, Cassandra, and MongoDB are a few examples.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Factors to Consider When Choosing a Database:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data Structure and Requirements&lt;/strong&gt;: NoSQL databases are better for unstructured or schema-less data, while relational databases are better for highly structured data that requires complex querying.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: Relational databases scale vertically by adding resources to a single server; NoSQL databases scale horizontally by sharing data across servers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: Relational databases perform better in ACID transactions, however NoSQL databases place a higher priority on low latency and high throughput.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Requirements for Consistency and Integrity&lt;/strong&gt;: Strict consistency and transactional integrity are guaranteed for accurate data by relational databases. NoSQL databases may provide models for eventual consistency, putting high availability ahead of instant consistency.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;from pymongo import MongoClient&lt;br&gt;
client = MongoClient('mongodb://localhost:27017/')&lt;br&gt;
db = client['my_database']&lt;br&gt;
collection = db['my_collection']&lt;br&gt;
collection.insert_one({'name': 'XYZ', 'age': 30, 'email': '*Emails are not allowed*'})&lt;br&gt;
# Query documents&lt;br&gt;
result = collection.find_one({'name': 'XYZ'})&lt;br&gt;
print(result)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;div-green&quot;&gt;&lt;span class=&quot;alert-header&quot;&gt;Tip:&lt;/span&gt; Prototype with different databases to assess performance, scalability, and suitability for your project before making a final decision.&lt;/div&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Python's Common Database Connection Methods&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;There are numerous ways to connect to databases in Python, including:&lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;1. Using built-in libraries&lt;/strong&gt;:&lt;/span&gt;&lt;/h3&gt;
&lt;/section&gt;
&lt;section&gt;Direct database connections without the need for additional dependencies are made possible by Python's built-in modules, such as sqlite3 for SQLite.&lt;br&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;import sqlite3&lt;br&gt;
conn = sqlite3.connect('example.db')&lt;br&gt;
cursor = conn.cursor()&lt;br&gt;
cursor.execute('SELECT * FROM table_name')&lt;br&gt;
results = cursor.fetchall()&lt;br&gt;
for x in results:&lt;br&gt;
  print(x)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;2. Third-party libraries:&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;External libraries provide additional functionality and optimizations for particular database systems, such as psycopg2 for PostgreSQL and mysql-connector-python for MySQL. These packages offer simple-to-use APIs for establishing database connections and running queries.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;import mysql.connector&lt;br&gt;
mydb= mysql.connector.connect(&lt;br&gt;
    host='hostname',&lt;br&gt;
    user='username',&lt;br&gt;
    database='database_name'&lt;br&gt;
)&lt;br&gt;
mycursor = mydb.cursor()&lt;br&gt;
mycursor.execute('SELECT * FROM table_name')&lt;br&gt;
myresult = mycursor.fetchall()&lt;br&gt;
for x in myresult:&lt;br&gt;
  print(x)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;img src=&quot;https://logiclair.org/?qa=blob&amp;amp;qa_blobid=11770887932542901517&quot; alt=&quot;&quot; width=&quot;750&quot; height=&quot;450&quot;&gt;
&lt;h3&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;3. ORM (Object-Relational Mapping) frameworks:&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;By abstracting database interactions, ORM frameworks such as SQLAlchemy save developers from writing raw SQL queries and enable them to work with Python objects. These frameworks offer strong capabilities for manipulating and managing databases.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;from sqlalchemy import create_engine, MetaData, Table&lt;br&gt;
engine = create_engine('postgresql://username:password@localhost/database_name')&lt;br&gt;
metadata = MetaData()&lt;br&gt;
table = Table('table_name', metadata, autoload=True, autoload_with=engine)&lt;br&gt;
results = engine.execute(table.select()).fetchall()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Connecting to Relational Database&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;There are multiple processes involved in setting up connections to widely used relational databases such as SQLite, MySQL, and PostgreSQL. Here's a thorough explanation with examples to help clarify:&lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;1. Setting up connections with each database:&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;A. SQLite:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Data is stored in a single file with the lightweight serverless database engine SQLite. With Python's built-in sqlite3 package, connecting to SQLite is simple.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;import sqlite3&lt;br&gt;
conn = sqlite3.connect('example.db')&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;img src=&quot;https://logiclair.org/?qa=blob&amp;amp;qa_blobid=14587128245889891539&quot; alt=&quot;&quot; width=&quot;750&quot; height=&quot;350&quot;&gt;
&lt;p&gt;&lt;strong&gt;B. MySQL:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The mysql-connector-python library can be used to create a connection for MySQL. Make sure pip install mysql-connector-python is installed on your computer.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;import mysql.connector&lt;br&gt;
conn = mysql.connector.connect(&lt;br&gt;
    host='localhost',&lt;br&gt;
    user='username',&lt;br&gt;
    database='database_name'&lt;br&gt;
)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;C. PostgreSQL:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can use the psycopg2 library to establish a connection to PostgreSQL. Install it (pip install psycopg2), please.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;import psycopg2&lt;br&gt;
conn = psycopg2.connect(&lt;br&gt;
    host='localhost',&lt;br&gt;
    user='username',&lt;br&gt;
    password='password',&lt;br&gt;
    database='database_name'&lt;br&gt;
)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;2. Handling connection errors and exceptions:&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;For your application to be robust, it is imperative that you handle exceptions and connection problems in a polite manner. Try-except blocks are a useful tool for handling and catching exceptions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;try:&lt;br&gt;
    conn = psycopg2.connect(&lt;br&gt;
        host='localhost',&lt;br&gt;
        user='username',&lt;br&gt;
        database='database_name'&lt;br&gt;
    )&lt;br&gt;
except psycopg2.Error as e:&lt;br&gt;
    print(f&quot;Error connecting to PostgreSQL: {e}&quot;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;3. Executing SQL queries and retrieving results:&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Once connected, you may use cursor objects to run SQL queries and get the appropriate responses.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;cursor = conn.cursor()&lt;br&gt;
cursor.execute('SELECT * FROM table_name')&lt;br&gt;
results = cursor.fetchall()&lt;br&gt;
for row in results:&lt;br&gt;
    print(row)&lt;br&gt;
cursor.close()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Connecting to NoSQL Databases&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;In Python, connecting to well-known NoSQL databases such as MongoDB and Redis requires simple steps for database operations, error handling, and setup:&lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;1. Setting up connections with each database:&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;A. MongoDB:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To create a connection for MongoDB, use the pymongo package.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;from pymongo import MongoClient&lt;br&gt;
client = MongoClient('mongodb://localhost:27017/')&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;B. Redis:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://pymongo.readthedocs.io/en/stable/&quot;&gt;Redis&lt;/a&gt; can be accessed by using the redis library.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;import redis&lt;br&gt;
r = redis.Redis(host='localhost', port=6379, db=0)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;2. Handling connection errors and exceptions:&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Make sure to use try-except blocks to properly handle connection problems.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;try:&lt;br&gt;
    client = MongoClient('mongodb://localhost:27017/')&lt;br&gt;
except Exception as e:&lt;br&gt;
  print(f&quot;Error connecting to MongoDB: {e}&quot;)&lt;br&gt;
// Redis&lt;br&gt;
try:&lt;br&gt;
    r = redis.Redis(host='localhost', port=6379, db=0)&lt;br&gt;
except Exception as e:&lt;br&gt;
    print(f&quot;Error connecting to Redis: {e}&quot;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;3. Executing database Operations:&lt;/strong&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Execute database operations like delete, update, and insert.&lt;/p&gt;
&lt;p&gt;MongoDB:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;db = client['my_database']&lt;br&gt;
collection = db['my_collection']&lt;br&gt;
collection.insert_one({'name': 'John', 'age': 30})&lt;br&gt;
collection.update_one({'name': 'John'}, {'$set': {'age': 31}})&lt;br&gt;
collection.delete_one({'name': 'John'})&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;r.set('key', 'value')&lt;br&gt;
value = r.get('key')&lt;br&gt;
r.delete('key')&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Error Handling&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Error handling in database connections is crucial for program reliability and robustness, preventing crashes and enhancing user experience by addressing unforeseen database errors and connection issues.&lt;/p&gt;
&lt;strong&gt;Using try-except blocks:&lt;/strong&gt; Try-except blocks can be used to catch and manage exceptions that are raised during database operations. This guarantees that your program can continue running even after encountering failures.&lt;br&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;import psycopg2&lt;br&gt;
try:&lt;br&gt;
    conn = psycopg2.connect(&lt;br&gt;
        host='localhost',&lt;br&gt;
        user='username',&lt;br&gt;
        database='database_name'&lt;br&gt;
    )&lt;br&gt;
except psycopg2.Error as e:&lt;br&gt;
    print(f&quot;Error connecting to PostgreSQL: {e}&quot;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;strong&gt;Handling specific error types&lt;/strong&gt;: To improve error management precision, classify errors such as query problems or connection failures and provide customised error messages and actions.&lt;br&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;import psycopg2&lt;br&gt;
try:&lt;br&gt;
    conn = psycopg2.connect(&lt;br&gt;
        host='localhost',&lt;br&gt;
        user='username',&lt;br&gt;
        password='password',&lt;br&gt;
        database='database_name'&lt;br&gt;
    )&lt;br&gt;
except psycopg2.OperationalError as e:&lt;br&gt;
    print(f&quot;Operational error connecting to PostgreSQL: {e}&quot;)&lt;br&gt;
except psycopg2.DatabaseError as e:&lt;br&gt;
    print(f&quot;Database error connecting to PostgreSQL: {e}&quot;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;strong&gt;Implementing retry mechanisms&lt;/strong&gt;: Retry techniques automatically retry database operations during temporary errors or network issues, enhancing application robustness, especially in unstable contexts.
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;import psycopg2&lt;br&gt;
import time&lt;br&gt;
max_retries = 3&lt;br&gt;
retry_delay = 1  # seconds&lt;br&gt;
for retry_count in range(max_retries):&lt;br&gt;
    try:&lt;br&gt;
        conn = psycopg2.connect(&lt;br&gt;
            host='localhost',&lt;br&gt;
            user='username',&lt;br&gt;
            database='database_name'&lt;br&gt;
        )&lt;br&gt;
        break&lt;br&gt;
    except psycopg2.Error as e:&lt;br&gt;
        print(f&quot;Error connecting to PostgreSQL: {e}&quot;)&lt;br&gt;
        if retry_count &amp;lt; max_retries - 1:&lt;br&gt;
            print(f&quot;Retrying in {retry_delay} seconds...&quot;)&lt;br&gt;
            time.sleep(retry_delay)&lt;br&gt;
        else:&lt;br&gt;
            print(&quot;Max retries exceeded, unable to connect.&quot;)&lt;br&gt;
          break&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Best Practices and Tips&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;By adhering to these best practices, Python programs may guarantee stable database connections:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;To effectively manage database connections, make use of connection pooling.&lt;/li&gt;
&lt;li&gt;In order to reduce overhead and boost efficiency, reuse connections.&lt;/li&gt;
&lt;li&gt;To liberate resources, always properly close connections.&lt;/li&gt;
&lt;li&gt;Handle connection credentials securely to avoid unwanted access.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;from sqlalchemy import create_engine, Pool&lt;br&gt;
# Create an engine with connection pooling&lt;br&gt;
engine = create_engine('database://user:password@host:port/db_name', pool_size=5, max_overflow=10)&lt;br&gt;
# Use the engine to connect to the database&lt;br&gt;
connection = engine.connect()&lt;br&gt;
# Perform database operations...&lt;br&gt;
# Close the connection&lt;br&gt;
connection.close()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/section&gt;
&lt;div class=&quot;div-faq&quot;&gt;&lt;span class=&quot;faq-header&quot;&gt;FAQ&lt;/span&gt; &lt;span class=&quot;faq-body&quot;&gt;&lt;br&gt;Q: Why is connection pooling important in database applications?&lt;br&gt;A: Connection pooling helps manage and reuse database connections, reducing the overhead of establishing new connections for each database operation. This improves performance and scalability, especially in applications with high traffic.&lt;/span&gt;&lt;/div&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;To sum up, this article highlights how important database connections are to software development. Comprehending the subtle differences between &lt;strong&gt;relational &lt;/strong&gt;and &lt;strong&gt;NoSQL database&lt;/strong&gt; types is essential for achieving the best possible project performance. Python provides &lt;strong&gt;flexible &lt;/strong&gt;ways to provide error handling and make database connections. Through careful consideration of database selection and adherence to industry best practices, developers can construct &lt;strong&gt;resilient systems&lt;/strong&gt; that effectively handle data and satisfy the requirements of contemporary software development.&lt;/p&gt;
&lt;/section&gt;
&lt;section&gt;
&lt;h2&gt;&lt;span style=&quot;font-family: arial,helvetica,sans-serif;&quot;&gt;&lt;strong&gt;Reference&lt;/strong&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;For more reference:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;PyMongo &lt;br&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://pymongo.readthedocs.io/en/stable/&quot;&gt;PyMongo - MongoDB Python driver&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;
</description>
<category>Data Structures</category>
<guid isPermaLink="true">https://coderlegion.com/390/database-interaction-connecting-to-databases</guid>
<pubDate>Fri, 26 Jul 2024 10:22:07 +0000</pubDate>
</item>
</channel>
</rss>