CyberHuginn

Home

/

Notes

/

when-redis-is-not-the-solution

When Redis Is Not the Solution

Redis is fast and extremely useful, but that doesn't make it the right solution for every backend problem. A practical look at when Redis helps with caching, rate limiting, OTPs, locks, queues, sessions, and temporary data—and when it simply adds unnecessary infrastructure and complexity.

Backend

Tip

Sep 19, 2026 · 9 min read

When Redis Is Not the Solution

Redis is fast.

That doesn’t mean you should use Redis for everything.

Redis is one of the most useful tools in a backend engineer’s toolbox. It is simple, fast, and flexible enough to solve many different problems.

That flexibility is also the problem.

Because Redis can solve many things, it is easy to introduce it before you actually need it.

A cache becomes Redis. An OTP becomes Redis. A lock becomes Redis. A queue becomes Redis. A temporary flag becomes Redis.

Eventually, Redis stops being a tool for a specific problem and becomes another dependency that every part of the system depends on.

The important question is not:

Can Redis solve this?

It usually can.

The better question is:

Does this problem actually need Redis?


Redis Is Not Free

When people talk about Redis, they usually talk about its performance.

That makes sense.

Redis is an in-memory data store with very low latency. But performance is only one part of an architectural decision.

Adding Redis also means adding:

  • another service to deploy
  • another connection pool to manage
  • another failure mode
  • another monitoring target
  • another configuration layer
  • another backup/recovery consideration
  • another piece of infrastructure developers need to understand

For a small application, these costs can be larger than the performance problem Redis was supposed to solve.

A database query taking 5ms is not necessarily a problem.

Adding an entire distributed system to avoid those 5ms might be.


Where Redis Makes Sense

There are many situations where Redis is exactly the right tool.

Caching

Redis works very well as a cache when the same expensive data is requested frequently.

For example:

Request
   ↓
Redis
   ↓
Cache hit → return
   ↓
Cache miss
   ↓
Database
   ↓
Redis

This is particularly useful when:

  • the underlying query is expensive
  • the data is read frequently
  • slightly stale data is acceptable
  • the application has enough traffic to justify caching

But caching everything is rarely a good strategy.

If a database query is already cheap, Redis can turn a simple read into:

Application → Redis → Cache miss → Database

instead of simply:

Application → Database

You have added another network hop and another failure point without necessarily improving anything.


Rate Limiting

Redis is a strong choice for distributed rate limiting.

For example, when multiple application instances need to share the same request counters:

        ┌── API instance
Client ─┼── API instance
        └── API instance
                │
                ↓
              Redis

Each instance needs to see the same rate-limit state.

This is a real distributed-state problem, and Redis is well suited for it.

But if your application has a single process and a simple local requirement, introducing Redis just for a few counters may be unnecessary.


OTP

OTP storage is another common Redis use case.

An OTP is temporary data:

phone → 483921
TTL   → 120 seconds

Redis makes this convenient because expiration is built into the data model.

But convenience doesn't automatically mean Redis is required.

If your existing database already supports expiration semantics well enough for your traffic and requirements, storing OTPs there can be simpler.

The important part is not whether Redis is faster.

The important part is whether the OTP flow requires a separate, short-lived data store.


Distributed Locks

Redis can implement distributed locks.

For example:

Worker A ─┐
          ├── Redis lock
Worker B ─┘

Only one worker should be allowed to perform a particular operation at a time.

This can be useful for things like:

  • preventing duplicate jobs
  • coordinating workers
  • avoiding concurrent processing of the same resource

But distributed locks are also one of the easiest places to introduce subtle bugs.

You now have to think about:

  • lock expiration
  • ownership
  • retries
  • network failures
  • process crashes
  • lock renewal
  • what happens when the lock expires while work is still running

If the database already provides the consistency primitive you need, a database transaction or row-level lock may be a much simpler solution.

For example, PostgreSQL's:

SELECT ... FOR UPDATE;

can sometimes solve the actual problem without introducing another distributed coordination mechanism.


Queues

Redis is often used as the backend for background task systems.

A typical architecture looks like:

API
 │
 ↓
Redis
 │
 ↓
Worker
 │
 ↓
Database / External Service

This is useful when you need:

  • background processing
  • delayed jobs
  • retries
  • multiple workers
  • asynchronous workloads

But there is an important distinction:

Redis is not a queue abstraction.

It is an infrastructure component that can be used to implement one.

If your application already has a reliable queue system, adding Redis only because a particular library supports it may not make architectural sense.

And if your workload is tiny, a full background-job infrastructure may itself be unnecessary.


Sessions

Redis can also be used for session storage.

This becomes particularly useful when multiple application instances need shared session state:

Client
  │
  ├── App 1 ─┐
  ├── App 2 ─┼── Redis
  └── App 3 ─┘

But before introducing Redis, ask whether the application actually needs server-side sessions.

For some architectures, stateless authentication with short-lived access tokens can remove the need for centralized session storage entirely.

Again, the question isn't:

Can Redis store sessions?

Of course it can.

The question is:

Do I need centralized session state?


Temporary Data

Redis is excellent for temporary data.

Anything that naturally looks like:

key → value
TTL → short

is a good candidate.

Examples include:

  • verification codes
  • temporary tokens
  • short-lived flags
  • throttling counters
  • temporary state during workflows

The TTL feature alone can make Redis very convenient.

But temporary data does not automatically mean Redis.

Sometimes the simplest solution is still the database you already have.


When Redis Adds Complexity

The biggest problem with Redis is usually not performance.

It is operational complexity.

Imagine a small Django application:

Django
PostgreSQL

A developer adds Redis for caching.

Now the system becomes:

Django
PostgreSQL
Redis

Then Redis is used for Celery:

Django
PostgreSQL
Redis
Celery
Workers

Then Redis becomes the OTP store.

Then rate limiting is added.

Then distributed locks.

Eventually:

             ┌── Cache
             │
             ├── OTP
             │
Django ──────┼── Rate Limit
             │
             ├── Locks
             │
             └── Celery
                    │
                 Redis

Redis has quietly become critical infrastructure.

If Redis goes down, suddenly:

  • authentication flows may fail
  • background jobs may stop
  • rate limiting may behave incorrectly
  • cache misses may increase database load
  • locks may stop working
  • temporary workflows may break

The original problem might have been:

One database query is a little slow.

The solution eventually became:

We now depend on Redis for half of the application.

That is not always a good trade.


Database First Is Not Bad Architecture

There is a tendency in backend development to treat the database as something that should be protected from application logic.

But modern databases are extremely capable.

PostgreSQL can provide:

  • indexes
  • transactions
  • row-level locks
  • constraints
  • JSON data
  • full-text search
  • advisory locks
  • efficient aggregation
  • connection pooling

Sometimes the simplest architecture is:

Application
     │
     ↓
PostgreSQL

Instead of:

Application
   │
   ├── Redis
   │
   └── PostgreSQL

The second architecture isn't automatically better because it has more components.


A Simple Decision Rule

Before adding Redis, ask a few questions.

1. Is there actually a performance problem?

Measure first.

If PostgreSQL handles the query in a few milliseconds and your application has low traffic, Redis probably isn't solving a real problem.

2. Does the data need to be shared across processes?

If the answer is no, process-local memory may sometimes be enough.

3. Does the data need a TTL?

If yes, Redis becomes more interesting.

But still ask whether your existing database can handle the requirement.

4. Is this a distributed coordination problem?

For locks, counters, queues, and shared state, Redis can be very useful.

But make sure the problem is actually distributed.

5. What happens if Redis goes down?

This is one of the most important questions.

If Redis is only a cache:

Redis down
   ↓
Database

That may be acceptable.

If Redis contains authentication state or coordinates critical workflows:

Redis down
   ↓
Application unavailable

That is a completely different architectural decision.

6. Can the database solve it safely?

Before introducing another infrastructure component, check whether the database already provides the primitive you need.

Sometimes:

Redis lock

can become:

Database transaction

Sometimes:

Redis counter

can become:

Database atomic update

And sometimes the database solution is actually easier to reason about.


Redis Should Be a Decision, Not a Default

Redis is an excellent tool.

The problem starts when it becomes the default answer to every backend problem.

Need caching?

Redis.

Need temporary data?

Redis.

Need a lock?

Redis.

Need a queue?

Redis.

Need a counter?

Redis.

Need sessions?

Redis.

This is how infrastructure grows without architecture growing with it.

A better approach is to start with the simplest system that satisfies the requirements.

Then measure.

Then identify the actual bottleneck.

Then introduce Redis where it provides a meaningful advantage.


The Real Question

The question isn't:

Is Redis fast?

It is.

The question isn't even:

Can Redis solve this?

It probably can.

The real question is:

What complexity am I willing to introduce to solve this problem?

Sometimes Redis is exactly the right answer.

Sometimes PostgreSQL is enough.

Sometimes in-process memory is enough.

Sometimes you don't need a new component at all.

Good backend architecture isn't about using powerful tools.

It is about using the right amount of infrastructure for the problem you actually have.

Redis is powerful.

That doesn't mean every problem deserves Redis.

Related Notes

The API Worked. The Architecture Didn’t.

A practical look at why a working API does not always mean a healthy architecture, covering coupling, synchronous work, database bottlenecks, caching, microservices, failure modes, and the importance of clear boundaries.

Django Transactions: What atomic() Actually Protects

Learn how Django transactions work, what transaction.atomic() actually protects, and how to handle race conditions, row locking, database constraints, nested transactions, and post-commit side effects with select_for_update() and on_commit().

The Hidden Cost of Django REST Framework Serializers

Learn how Django REST Framework serializers can cause N+1 queries, slow API responses, and unnecessary database work—and how to optimize them with select_related, prefetch_related, annotations, and better serializer design.

Building a Django Package — Part 4: Publishing django-healthkit to PyPI

In this final part, we prepare django-healthkit for release, build and validate the package, test it on TestPyPI, publish it to PyPI, and create a Git tag and GitHub release.

Building a Django Package — Part 3: Health Check Manager and Endpoint

In Part 3, we connect the database and cache health checks, build the health check manager, expose a Django health endpoint, measure check latency, and return a structured health status.

Building a Django Package — Part 2: Database and Cache Health Checks

In this part, we implement the first health checks for django-healthkit, covering database connectivity and Django cache functionality with simple, independent, and testable checks.

Building a Django Package — Part 1: Setting Up django-healthkit

In this part, we build the initial structure of django-healthkit, configure the package with pyproject.toml, and prepare it for development.

Building a Secure Webhook Receiver for Server-to-Server Communication

How I built a secure FastAPI webhook receiver using RSA signatures to enable authenticated server-to-server communication, proxy requests, and connect applications across different network environments.

Why I Switched to Conventional Git Commit Messages

Why I switched from inconsistent Git commit messages to a Conventional Commits style, with practical examples for cleaner and more maintainable Git history.

Building django-healthkit

How a simple health endpoint for Bidar turned into django-healthkit, a lightweight Django health-check package.

Designing a Gold Jewelry E-Commerce Database with Django

Learn how to design a scalable Django database for a gold jewelry e-commerce platform by modeling products, attributes, and purchasable product variants using real-world domain-driven design principles.

How to Fix Common Next.js 16 Build Errors (Proxy, Dynamic Rendering & Revalidation)

Learn how to fix common Next.js 16 build errors, understand Proxy, Dynamic Rendering, and Revalidation, and improve your application's SEO and performance.

Why I Built cyber-ui: A Minimal Design System for Developers

How my projects, from backend systems to monitoring tools, led me to create a minimal and personal UI foundation.

End of note.