Skip to content
ExponaBLOG
MONGODB, NOSQL & DATA MODELING · DAY 071 / 100

MongoDB vs Redis: a MongoDB & NoSQL guide for developers

Different tools for different jobs.

Expona Admin8 min read

We build systems that need to move fast and stay reliable, which means choosing the right tool for each job. MongoDB vs Redis: a MongoDB & NoSQL guide for developers isn't really a competition—they solve different problems, and on most production teams, we end up using both. This guide walks through when to reach for each and how to combine them without losing your data.

Redis: in-memory cache and data structures

Redis lives in RAM. It's purpose-built for speed, not durability. We use it when we need to answer questions in microseconds: sessions, leaderboards, real-time counters, rate limiting, and task queues. If you're building an e-commerce platform, Redis holds the shopping cart; it doesn't store the order history.

The appeal is real. A single Redis instance can handle hundreds of thousands of operations per second. Its data structures—strings, lists, sets, sorted sets, streams, hashes—map naturally to common patterns. For example, a sorted set is perfect for a leaderboard:

javascript
// Add player score to leaderboard (sorted set)
await redis.zadd('leaderboard', score, playerId);

// Get top 10
const topTen = await redis.zrevrange('leaderboard', 0, 9, 'WITHSCORES');

// Get player rank
const rank = await redis.zrevrank('leaderboard', playerId);

Another common case: session management. Store a user session in Redis and set an expiration:

javascript
// Store session for 1 hour
await redis.setex(`session:${sessionId}`, 3600, JSON.stringify({
  userId: 123,
  email: 'user@example.com',
  permissions: ['read', 'write']
}));

// Later: retrieve and check expiry
const session = await redis.get(`session:${sessionId}`);

Redis also shines for rate limiting. Using a simple key pattern and expiration:

javascript
const key = `rate_limit:${userId}:${Math.floor(Date.now() / 60000)}`;
const count = await redis.incr(key);
await redis.expire(key, 60);

if (count > REQUEST_LIMIT) {
  throw new Error('Rate limit exceeded');
}

The catch: Redis data lives in memory. Restart the server, and everything is gone—unless you enable persistence (RDB snapshots or AOF). Even with persistence, Redis isn't built for consistency guarantees like MongoDB. It's a cache, not a database.

MongoDB: durable document storage

MongoDB stores documents on disk. It's your source of truth. Orders, users, products, audit logs—these belong in MongoDB. It offers ACID transactions, replication across multiple nodes, and retrieval patterns tailored to documents.

If Redis answers "what is this value right now?" in microseconds, MongoDB answers "find all orders for this customer placed in the last 30 days" and survives a server crash with zero data loss (in a replica set).

The document model is flexible. You can nest related data without joins:

javascript
// Insert order with line items embedded
db.orders.insertOne({
  _id: ObjectId(),
  userId: 123,
  createdAt: new Date(),
  items: [
    { productId: 'SKU-001', quantity: 2, price: 29.99 },
    { productId: 'SKU-002', quantity: 1, price: 49.99 }
  ],
  shippingAddress: {
    street: '123 Main St',
    city: 'Seattle',
    state: 'WA'
  },
  status: 'pending'
});

// Query orders for a user
const orders = await db.orders.find({ userId: 123, status: 'pending' }).toArray();

Indexes and aggregation pipelines let you perform complex queries without fetching everything into memory:

javascript
// Aggregate: group orders by month, sum totals
const monthlyRevenue = await db.orders.aggregate([
  { $match: { status: 'completed', createdAt: { $gte: startDate } } },
  { $group: { _id: { $dateToString: { format: '%Y-%m', date: '$createdAt' } }, total: { $sum: '$total' } } },
  { $sort: { _id: 1 } }
]).toArray();

ACID transactions (introduced in MongoDB 4.0) mean we can update multiple documents atomically:

javascript
const session = client.startSession();
try {
  await session.withTransaction(async () => {
    // Debit account
    await accounts.updateOne({ _id: fromId }, { $inc: { balance: -amount } }, { session });
    // Credit account
    await accounts.updateOne({ _id: toId }, { $inc: { balance: amount } }, { session });
  });
} finally {
  await session.endSession();
}

MongoDB is reliable for data you can't afford to lose. The trade-off: it's slower than Redis. A MongoDB query might take 10–100ms; Redis gives you the answer in 1ms.

Use both together

The smart move is combining them. MongoDB holds the truth; Redis makes it fast. A typical pattern:

Check Redis first, fall back to MongoDB. We call this cache-aside:

javascript
async function getUser(userId) {
  // Try cache
  const cached = await redis.get(`user:${userId}`);
  if (cached) return JSON.parse(cached);

  // Cache miss: fetch from MongoDB
  const user = await db.users.findOne({ _id: userId });
  
  // Store in cache for 1 hour
  await redis.setex(`user:${userId}`, 3600, JSON.stringify(user));
  
  return user;
}

When data changes, invalidate the cache. On a user profile update:

javascript
async function updateUser(userId, updates) {
  // Update source of truth
  const result = await db.users.updateOne(
    { _id: userId },
    { $set: updates }
  );

  // Invalidate cache
  await redis.del(`user:${userId}`);

  return result;
}

This pattern keeps reads fast and ensures consistency: if data gets out of sync, the cache eventually expires and is refreshed from MongoDB.

Another example: product catalog. High-traffic reads benefit enormously from Redis:

javascript
async function getProduct(productId) {
  const key = `product:${productId}`;
  let product = await redis.get(key);
  
  if (!product) {
    product = await db.products.findOne({ _id: productId });
    if (product) {
      await redis.setex(key, 7200, JSON.stringify(product));
    }
  }
  
  return product ? JSON.parse(product) : null;
}

For features like shopping carts (ephemeral, high-volume writes), Redis alone is fine:

javascript
// Cart lives in Redis; we write it to MongoDB on checkout
async function addToCart(userId, productId, quantity) {
  const key = `cart:${userId}`;
  await redis.hset(key, productId, quantity);
  await redis.expire(key, 86400); // 24 hours
}

async function checkout(userId) {
  const cartKey = `cart:${userId}`;
  const cart = await redis.hgetall(cartKey);
  
  // Create order in MongoDB
  const order = await db.orders.insertOne({
    userId,
    items: Object.entries(cart).map(([productId, quantity]) => ({ productId, quantity })),
    createdAt: new Date()
  });

  // Clear cart
  await redis.del(cartKey);
  
  return order;
}

Do not use Redis as the only source of truth

This is the hard lesson many teams learn the expensive way. Redis has no persistence by default, and even with AOF (append-only file) or RDB snapshots enabled, recovery is slower and less reliable than MongoDB's replication.

If you store an order only in Redis and the server crashes before AOF syncs to disk, that order vanishes. A customer calls support: "Where's my order?" You have no record. That's a bad day.

Redis is designed to be eventually consistent with a backing store. It's a cache layer, not a database. Even using Redis Cluster or Redis Sentinel for high availability doesn't change this—a cluster-wide failure can still lose data.

Similarly, never use MongoDB as a cache if you can avoid it. Writing every session or cart update to MongoDB burns through disk I/O and makes queries slower. That's what Redis is for.

Some teams try to use Redis as a message queue for critical operations. Don't. If a job is important, use a proper queue—Kafka, RabbitMQ, or Redis Streams (which persist to disk when configured). Or better, use MongoDB's oplog as an event source if you're building event-driven architecture.

Common mistakes

  • Treating Redis as durable: It's not. Always have a source of truth in MongoDB (or another durable store). Use Redis to speed up access, not to store authoritative data.
  • Not invalidating cache: Stale data is worse than no data. When updating records, invalidate Redis keys immediately. Use a TTL so stale data expires anyway.
  • Storing everything in Redis: If you need to query it flexibly or retain it long-term, MongoDB is the right tool. Redis is for hot, ephemeral, or frequently accessed data with simple access patterns.
  • Ignoring serialization overhead: Serializing and deserializing JSON in Redis adds latency. For high-throughput scenarios, consider storing compressed or binary data, or use Redis native types (hashes, sets) when they fit your model.
  • Forgetting about memory limits: Redis runs out of memory. Set a maxmemory policy (LRU, LFU) and monitor usage. If your data grows unbounded, MongoDB is the storage layer you need.
  • Coupling application code to cache keys: Cache keys scatter across your codebase. Use a cache layer abstraction or centralize key naming conventions to avoid bugs when keys change.

Key takeaways

  • Redis is an in-memory data structure store for speed; MongoDB is a durable document database for truth. They solve different problems.
  • Use Redis for sessions, caches, counters, leaderboards, and rate limiting. Use MongoDB for orders, users, products, and any data you cannot afford to lose.
  • Combine them: check Redis first, fall back to MongoDB, and invalidate cache keys on writes.
  • Never use Redis as your only source of truth. Always have a persistent backing store.
  • Monitor Redis memory usage and set eviction policies. Don't let unbounded data crash your cache.
  • Keep cache invalidation simple: TTLs are your friend. Sync-on-write patterns (check cache, miss, fetch, store, expire) are easier to reason about than complex invalidation rules.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles