Every Redis key has one type (string, list, hash, set, zset) and commands are type-strict. You asked a hash command of a string key — often code drift or a key-collision between features.
Feature A stored user:123 as a string, feature B treats it as a hash (HSET). TYPE user:123 and HGETALL/GET reveal what's actually there. The collision usually spans two codepaths.
Code moved from SET to HSET (or added JSON strings where lists used to live) without migrating existing keys. TTL'd keys self-heal; long-lived ones keep failing.
redis-cli TYPE <key> ; redis-cli TTL <key> ; redis-cli OBJECT ENCODING <key>
redis-cli --scan --pattern '<prefix>:*' | head -5 ; # then DEL or convert per key after confirming ownership
# featureA:user:123 vs featureB:user:123 — one key, two meanings is the design bug
const t = await redis.type(key); const val = t === 'hash' ? await redis.hgetall(key) : JSON.parse(await redis.get(key));
Redis Studio/insight or redis-cli --bigkeys help audit type collisions across namespaces. WRONGTYPE is thrown before any data is touched — it's safe to catch and branch on in a migration.
Commands are implemented per underlying data structure (you can't LPUSH a string). The type is part of the key's identity, so shared key names between features are landmines.
Key-naming conventions with feature prefixes (and a light schema doc) prevent most collisions. For JSON-vs-structure changes, version the key name (user:123:v2) instead of reusing the old one.
Our most-documented failures, packaged as ready-to-ship starter kits: Docker, Kubernetes, and Terraform.
Browse the template store →One-time. Yours to modify. Instant download from the NinjaOps template store.