Elasticsearch vs Redis: Comparing Approaches to Smart Caching and Search in Microservice Architecture
Introduction: Why Compare Elasticsearch and Redis at All?
At first glance, comparing Elasticsearch and Redis seems odd: one is a powerful search engine built on Lucene, the other an in-memory data store with rich data structures. Yet in real-world microservice systems, these tools often compete for the same niche — fast data access and relevant search.
The reason for the comparison is straightforward: both technologies operate in memory (or partially so), both support horizontal scaling, and both can respond to queries in milliseconds. When a team is designing a new microservice with search or caching functionality, the question arises: do we need a full-blown Elasticsearch, or will Redis Search do the job?
In this article, we'll examine both tools without marketing spin — complete with architectural diagrams, Go code examples, and practical recommendations applicable in 2026.
Redis as a Cache: Patterns, TTL, and Eviction Policies
Redis is the gold-standard caching tool in microservice systems. Its single-threaded event loop architecture delivers predictable latency: a typical GET takes under 1 ms even with thousands of concurrent connections.
Cache-Aside Pattern
The most common pattern. The application checks the cache first; on a miss, it goes to the primary store and writes the result back to Redis.
// Go: Cache-Aside with Redis
func GetProduct(ctx context.Context, rdb *redis.Client, db *sql.DB, id string) (*Product, error) {
cacheKey := "product:" + id
// 1. Check the cache
cached, err := rdb.Get(ctx, cacheKey).Bytes()
if err == nil {
var p Product
if json.Unmarshal(cached, &p) == nil {
return &p, nil
}
}
// 2. Cache miss — query the DB
var p Product
err = db.QueryRowContext(ctx,
"SELECT id, name, price FROM products WHERE id = $1", id,
).Scan(&p.ID, &p.Name, &p.Price)
if err != nil {
return nil, err
}
// 3. Store in cache with a 10-minute TTL
data, _ := json.Marshal(p)
rdb.Set(ctx, cacheKey, data, 10*time.Minute)
return &p, nil
}
Write-Through Pattern
Every write to the database simultaneously updates the cache. This guarantees consistency but increases write latency. It is used where data freshness is critical — for example, in inventory services.
TTL and Eviction Policies
Redis supports several eviction policies configured via maxmemory-policy in the configuration file:
- allkeys-lru — evicts the least recently used keys. A solid default choice for a cache.
- volatile-lru — evicts only keys that have a TTL set. Safer when some data must remain persistent.
- allkeys-lfu — evicts the least frequently used keys. Better for workloads with a clear hot set.
- noeviction — returns an error when the memory limit is reached. Dangerous for caching scenarios.
# redis.conf for a caching node
maxmemory 4gb
maxmemory-policy allkeys-lru
maxmemory-samples 10
# Lazy memory freeing (recommended for high-load environments)
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
Elasticsearch as a Search Engine: Full-Text Search and Aggregations
Elasticsearch solves a fundamentally different problem. Its strength lies in relevant full-text search powered by an inverted index, real-time aggregations across millions of documents, and a flexible DSL query language.
How Relevance Scoring Works
Elasticsearch uses the BM25 algorithm (the default since version 5.0) to rank results. A document's score depends on term frequency within the document, the rarity of the term across the collection (IDF), and field length. This fundamentally distinguishes it from simple index-based lookups in Redis.
Example Query with Aggregation
// Go: full-text search with aggregation via Elasticsearch
func SearchProducts(ctx context.Context, es *elasticsearch.Client, query string) error {
body := map[string]interface{}{
"query": map[string]interface{}{
"multi_match": map[string]interface{}{
"query": query,
"fields": []string{"name^3", "description", "tags"},
"type": "best_fields",
"fuzziness": "AUTO",
},
},
"aggs": map[string]interface{}{
"by_category": map[string]interface{}{
"terms": map[string]interface{}{
"field": "category.keyword",
"size": 10,
},
},
"price_stats": map[string]interface{}{
"stats": map[string]interface{}{
"field": "price",
},
},
},
"highlight": map[string]interface{}{
"fields": map[string]interface{}{
"name": map[string]interface{}{},
"description": map[string]interface{}{},
},
},
}
data, _ := json.Marshal(body)
res, err := es.Search(
es.Search.WithContext(ctx),
es.Search.WithIndex("products"),
es.Search.WithBody(bytes.NewReader(data)),
es.Search.WithTrackTotalHits(true),
)
if err != nil {
return err
}
defer res.Body.Close()
// process results...
return nil
}
When Elasticsearch Is Indispensable
- Full-text search with relevance ranking and result highlighting.
- Faceted filtering with aggregations (e-commerce, product catalogs).
- Log and metrics analysis (ELK stack).
- Search across multilingual data with morphological analysis.
- Indexes of several million documents or more.
Hybrid Scenarios: When Redis and Elasticsearch Work Together
In practice, Redis and Elasticsearch don't compete — they complement each other. A typical search microservice architecture looks like this:
Client → API Gateway → Search Service → [Redis Cache] → Elasticsearch → PostgreSQL (data source)
Architectural diagram (text description):
- The client sends a search request to the Search Service.
- The Search Service generates a cache key based on a hash of the request parameters.
- Redis is checked: if the key exists, the cached JSON with results and aggregations is returned.
- On a cache miss, the request is forwarded to the Elasticsearch cluster.
- The result is serialized and stored in Redis with a TTL (30–300 seconds depending on the query type).
- Asynchronously: an indexer listens for change events in PostgreSQL via Debezium/CDC, updates the Elasticsearch index, and invalidates the corresponding Redis keys.
This approach reduces load on Elasticsearch by 60–80% in scenarios with repeated search queries (catalogs, home pages, popular categories).
Case Study 1: Caching Elasticsearch Search Results in Redis
Solution Architecture
The key question is how to construct a cache key for search queries. The naive approach (using the raw query string) doesn't work: parameters may arrive in different orders, and filters may appear in varying combinations.
// Go: deterministic cache key for a search request
type SearchRequest struct {
Query string `json:"query"`
Filters map[string]string `json:"filters"`
Page int `json:"page"`
PageSize int `json:"page_size"`
SortField string `json:"sort_field"`
SortOrder string `json:"sort_order"`
}
func (r *SearchRequest) CacheKey() string {
// Sort filter keys for determinism
keys := make([]string, 0, len(r.Filters))
for k := range r.Filters {
keys = append(keys, k)
}
sort.Strings(keys)
parts := []string{
"search",
url.QueryEscape(strings.ToLower(r.Query)),
fmt.Sprintf("p%d", r.Page),
fmt.Sprintf("ps%d", r.PageSize),
r.SortField + ":" + r.SortOrder,
}
for _, k := range keys {
parts = append(parts, k+"="+r.Filters[k])
}
raw := strings.Join(parts, "|")
hash := sha256.Sum256([]byte(raw))
return "es_cache:" + hex.EncodeToString(hash[:16])
}
// Service with caching
type SearchService struct {
es *elasticsearch.Client
rdb *redis.Client
}
func (s *SearchService) Search(ctx context.Context, req *SearchRequest) (*SearchResult, error) {
key := req.CacheKey()
// Attempt to retrieve from cache
if cached, err := s.rdb.Get(ctx, key).Bytes(); err == nil {
var result SearchResult
if json.Unmarshal(cached, &result) == nil {
result.FromCache = true
return &result, nil
}
}
// Query Elasticsearch
result, err := s.queryElasticsearch(ctx, req)
if err != nil {
return nil, err
}
// Cache with adaptive TTL
ttl := s.adaptiveTTL(req)
if data, err := json.Marshal(result); err == nil {
s.rdb.Set(ctx, key, data, ttl)
}
return result, nil
}
// Adaptive TTL: popular queries are cached longer
func (s *SearchService) adaptiveTTL(req *SearchRequest) time.Duration {
if req.Page == 1 && req.Query == "" {
return 5 * time.Minute // catalog home page
}
if req.Page > 5 {
return 30 * time.Second // deep pagination — short TTL
}
return 2 * time.Minute
}
Pitfalls
- Cache invalidation on index updates. When documents change in Elasticsearch, the corresponding Redis keys must be invalidated. The challenge is that a single document can affect thousands of search queries. Solutions: tag-based invalidation (Redis Tags via Lua scripts or an external mapping) or a short TTL with tolerable staleness.
- Cache stampede. When many keys expire simultaneously, all requests hit Elasticsearch at once. Solutions: probabilistic early expiration or a distributed lock via
SET NX. - Aggregation serialization. An Elasticsearch response including aggregations can be several MB in size. Compressing with gzip before writing to Redis reduces memory usage by 70–80%.
Case Study 2: Redis Search as an Alternative to Elasticsearch
Redis Search (the RediSearch module, part of Redis Stack) is a serious alternative for scenarios with data volumes up to a few million documents. It supports full-text search, numeric filters, geo-search, and aggregations — all in memory.
When Redis Search Is Justified
- Up to 1–5 million documents with a compact structure.
- The team doesn't want to maintain a separate Elasticsearch cluster.
- Atomic updates of data and index are required (Redis stores both).
- Search latency must be < 5 ms.
// Go: Redis Search using go-redis v9
func CreateIndex(ctx context.Context, rdb *redis.Client) error {
_, err := rdb.Do(ctx, "FT.CREATE", "products_idx",
"ON", "HASH",
"PREFIX", "1", "product:",
"SCHEMA",
"name", "TEXT", "WEIGHT", "5.0",
"description", "TEXT",
"category", "TAG",
"price", "NUMERIC", "SORTABLE",
"stock", "NUMERIC",
).Result()
return err
}
func SearchInRedis(ctx context.Context, rdb *redis.Client, query, category string, maxPrice float64) (interface{}, error) {
// Full-text search + tag and numeric field filtering
ftQuery := fmt.Sprintf("@name|description:(%s) @category:{%s} @price:[0 %v]",
query, category, maxPrice,
)
result, err := rdb.Do(ctx, "FT.SEARCH", "products_idx",
ftQuery,
"LIMIT", "0", "20",
"SORTBY", "price", "ASC",
"HIGHLIGHT", "FIELDS", "2", "name", "description",
).Result()
return result, err
}
Limitations of Redis Search
- All data is stored in RAM — storage costs are significantly higher than Elasticsearch with disk-based indexes.
- No support for nested objects at the level of Elasticsearch.
- Aggregations are less expressive: no pipeline aggregations, no percentile aggregations.
- BM25 scoring is less configurable than in Elasticsearch.
- Horizontal scaling via Redis Cluster has limitations around cross-slot operations.
Tool Comparison: Performance, Scalability, and Cost
Performance
- Redis (cache): < 1 ms per GET/SET, 100k+ ops/sec on a single node. Ideal for hot data.
- Redis Search: 1–5 ms per search query with an index of up to 1 million documents. Degrades as volume grows.
- Elasticsearch: 10–100 ms for a complex search query with aggregations, but scales linearly to billions of documents. A warm cache (OS file cache) can bring this down to 5–20 ms.
Scalability
- Redis Cluster: horizontal scaling via sharding. Limitation: multi-key operations only work within a single slot. Redis Sentinel provides HA without sharding.
- Elasticsearch: native horizontal scaling via shards and replicas. Supports cross-datacenter replication (CCR). Operations across an entire index are not subject to Redis Cluster limitations.
Operational Complexity
- Redis: simple setup, minimal configuration, predictable behavior. Monitoring via
redis-cli INFOand built-in metrics. Backup via RDB/AOF. - Elasticsearch: requires JVM heap tuning (recommended at 50% of RAM, but no more than 31 GB for compressed oops), shard management, and cluster health monitoring. Running a cluster of 3+ nodes demands considerable operational effort from the team.
Total Cost of Ownership (TCO)
- Redis OSS: free, but Redis Stack (with Search) requires a license for production use of certain features. Managed Redis (AWS ElastiCache, GCP Memorystore) — from $0.02/hour per node.
- Elasticsearch OSS / OpenSearch: free. Elastic Cloud or AWS OpenSearch Service — from $0.10/hour per node. A minimal production configuration (3 dedicated masters + 2 data nodes) costs $300–600/month on the cloud.
Practical Recommendations: What to Choose and When
Choose Redis (without Search) if:
- The task is caching results, sessions, rate limiting, or pub/sub between microservices.
- Search amounts to exact key lookups or simple filtering.
- The team is small (< 5 backend developers) and operational overhead is a critical concern.
Choose Redis Search if:
- Data volume is < 2 million documents and the document structure is compact (< 1 KB).
- You're already using Redis and want to avoid adding another infrastructure component.
- Atomic updates of data and index are required.
- Infrastructure budget is limited and the search SLA is > 50 ms (not real-time).
Choose Elasticsearch if:
- Data volume exceeds 5 million documents or rapid growth is expected.
- Relevant full-text search with morphology, synonyms, and highlighting is needed.
- Complex aggregations are required: nested, pipeline, percentile, geo.
- An ELK stack is already in place for logging — the infrastructure can be reused.
- Search SLA is < 100 ms under load of > 1,000 QPS.
Hybrid Approach: Recommended Architecture for High-Load Systems
In high-load systems with demanding search requirements, use both tools for their intended purposes:
- Elasticsearch — the source of truth for search queries, aggregations, and facets.
- Redis — an L1 cache on top of Elasticsearch with TTL and adaptive caching of popular queries.
- Redis also — a cache for auth tokens, rate limiting, and task queues (via Redis Streams).
- Invalidation — via CDC (Debezium) or internal microservice events.
Conclusion: There Is No Universal Answer — Only the Right Context
Elasticsearch and Redis are not competitors; they are tools with fundamentally different profiles. Redis excels as a fast cache with rich data structures and a simple operational model. Elasticsearch is indispensable for relevant full-text search over large data volumes with flexible aggregations.
Redis Search fills the middle ground and is often the pragmatic choice for startups and smaller services where operational simplicity outweighs the functional capabilities of Elasticsearch.
The right answer to "what should I choose" is determined not by a tool's popularity, but by the specific characteristics of your system: data volume, relevance requirements, SLA, team size, and infrastructure budget. Start with Redis — and move to Elasticsearch when Redis Search can no longer keep up. That's not a compromise; it's sound engineering pragmatism.
Technologies
Tags
Ruslan Ismailov
Senior Web / Backend Developer. Senior web/backend developer with 9 years of experience. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservices, CI/CD. More about me →