Migrating from Memcached to Redis
The good news: cache contents migrate themselves through normal write traffic, no copy step needed. The work is in the application layer: dual-write, shadowing, cutover, decommission. The pattern is well-trodden and the risk is manageable when staged carefully.
Why teams actually migrate
Migration projects rarely start with "Redis is fundamentally better." They usually start with a specific product requirement that Memcached cannot satisfy. The PM wants a leaderboard feature: that requires sorted sets, which Memcached does not have. The reliability team wants session persistence: that requires RDB or AOF, which Memcached does not have. The platform team wants centralised cache invalidation across a fleet of API servers: that requires pub/sub, which Memcached does not have.
When the requirement lands, the team faces a choice: stand up a new Redis store alongside the existing Memcached for the specific feature, or migrate everything to Redis. The "alongside" path is operationally heavier (two cache systems to monitor, two to scale, two to patch). The migration path is heavier upfront but lighter long-term. Most teams that hit two or three Redis-only requirements end up migrating.
Pure-cache migrations (no new feature requirement, just "we should be on Redis") are harder to justify. Memcached is a fine pure cache and the migration carries operational risk. The rare case where pure-cache migration makes sense is when the broader infrastructure is consolidating onto cloud-managed services and the team wants to drop one of the two managed offerings to simplify ops. Even then, the saving is mostly headcount, not infrastructure cost.
The dual-write pattern
The canonical migration pattern is dual-write with shadow-read. Phase 1 provisions the Redis store alongside the running Memcached, with no application changes. Phase 2 adds dual-write: every cache.set() in the application writes to both stores before returning, and every cache.delete() deletes from both. Reads continue to come from Memcached. The Redis store gradually warms with the same data that lives in Memcached.
Phase 3 adds shadow-read: every cache.get() reads from both stores, returns the Memcached value to the application, and asynchronously compares the Redis value. Discrepancies are logged. This phase is the validation step; you let it run until the discrepancy rate is low enough to trust the Redis store. Common sources of discrepancy: different serialization formats (Memcached returning bytes, Redis returning typed values), different TTL handling, different handling of large values.
Phase 4 cuts over the read path: cache.get() now reads from Redis first, falls back to Memcached if Redis missing (which should be rare once the dual-write has been running for a TTL period). Phase 5 removes the Memcached fallback after a further bake period. Phase 6 decommissions the Memcached infrastructure. Each phase has a documented rollback (revert to the previous phase's behaviour) and the migration can pause at any phase if issues surface.
The traps to plan for
Key collision is the most common source of bugs. If your Memcached and Redis instances share key namespaces but your application has subtle behaviour that depends on Memcached-specific semantics (auto-truncation of long keys, key-character restrictions, the 250-byte key length limit), the Redis side may store keys differently. Normalise key handling in a single cache-key builder function before splitting writes between stores.
Value serialization differs subtly. Memcached's text protocol returns values as bytes; the client library usually deserializes (gomemcache stores raw bytes by default, php Memcached extension auto-serializes). Redis values via redis-py, ioredis, StackExchange.Redis are typed differently. If you JSON-encode on write and JSON-decode on read in both, this is fine. If you rely on the client library's auto-serialization, the formats may diverge.
TTL handling has a sharp edge. Memcached treats TTLs of 30 days or less as relative seconds and TTLs over 30 days as absolute Unix timestamps. Redis treats all TTLs as relative seconds. An application that sets a 60-day TTL on Memcached (interpreted as a Unix timestamp in 2025) and then writes the same TTL to Redis (interpreted as 60 days from now) will see different expiry behaviour. Normalise TTLs to always-relative in your code before writing to either store.
The cutover decision
The moment of greatest risk is the read-path cutover. Up to this point, the application has been serving real traffic from Memcached; any bugs in the Redis store have been invisible to users. After cutover, every read goes to Redis and any latency regression, error rate spike, or data correctness problem becomes user-visible immediately.
The mitigation is gradual percentage cutover. Start with 1% of read traffic going to Redis (the rest still on Memcached). Monitor for an hour, then a day. If stable, ramp to 10%, then 50%, then 100% over several days. At each step you have a fast rollback (drop back to the previous percentage by changing one feature flag). A full bad-state restoration usually takes minutes, not hours.
Once 100% of reads have been on Redis for a week with no incidents, remove the Memcached fallback code. Wait another bake period (typically one full TTL cycle plus a safety margin) and decommission the Memcached infrastructure. The whole migration, soup to nuts for a moderately complex application, is usually a 4-8 week project including the prep work, the dual-write phase, the cutover, and the decommission.
FAQ
Why migrate from Memcached to Redis?
Usually because a new feature requires something Memcached cannot do: persistence for sessions, sorted sets for leaderboards, pub/sub for invalidation, atomic operations for rate limiting. The case for a pure-cache migration (no new feature requirements) is weaker; Memcached is fine for pure caching.
Can I migrate cache contents directly?
Usually not. Memcached is ephemeral by design. The simpler approach is to run both stores in parallel for a TTL period (typically 1 hour to 30 days depending on cache key lifespans), letting the new Redis store warm naturally as cache keys are written. Then cut over the read path and decommission Memcached.
What is dual-write?
On every cache write, the application writes to both the old store (Memcached) and the new store (Redis) before returning to the user. On reads, you can choose: read from Memcached first then Redis if missing (gradual warm), read from Redis first then Memcached fallback (verify-mode), or shadow-read from both and compare (correctness validation).
What can break?
Cache key collisions if the same key is used differently in the two stores. Serialization format differences (Memcached often stores raw bytes, Redis often stores typed values via the client library). TTL handling (Memcached uses absolute timestamps for TTLs over 30 days, Redis uses relative). Client library API differences if you wrap the cache calls. Test all of these in a staging environment before production.
How long should the dual-write phase last?
Long enough to validate behaviour at full production traffic plus the longest TTL in your cache. For a cache with 24h TTLs, 7 days of dual-write is a reasonable minimum to catch edge cases. For caches with 30-day TTLs, expect a longer bake. Cut over only when you are confident the new store is correctly serving production traffic.