DEL, EXISTS, and TYPE

Every key in Redis lives in a single flat keyspace, and three commands let you manage that keyspace directly: DEL removes one or more keys outright, EXISTS tells you whether a key is currently present, and TYPE tells you what kind of value a key holds — string, list, hash, set, sorted set, or stream. These are among the most frequently used commands in any Redis application, because almost every real operation starts with "does this key exist?" or "what am I working with here?" before deciding what to do next.

Overview: How Redis Tracks Keys and Types

Internally, Redis stores every database as one large hash table (the "main dict") mapping key names to a pointer to a redisObject. Every redisObject carries a type tag — string, list, hash, set, sorted set, or stream — set once when the key is first created and never changed for that key’s lifetime. This is why Redis keys are described as "typed": a key doesn’t just hold bytes, it holds a specific structure, and every command that touches a key first checks that its type tag matches what the command expects. That check is what produces the WRONGTYPE error you’ll see later in this lesson.

EXISTS and TYPE are both constant-time lookups against that same main dict — EXISTS checks whether the hash table has an entry for the key, while TYPE follows the pointer and reads back the type tag. Neither command inspects the value’s contents, so checking the type of a million-element hash is exactly as fast as checking a one-character string.

DEL is different: it has to actually free the memory the value occupies. For a key holding a string, that’s a single O(1) deallocation. For a key holding a list, hash, set, sorted set, or stream, Redis has to walk the whole structure and free every internal node, making that key’s contribution to the command O(M) where M is the number of elements it holds. Because Redis is single-threaded, that walk happens inline on the main thread — deleting one enormous collection can measurably stall every other client for the duration of the delete. (Since Redis 4.0, UNLINK exists as an asynchronous alternative that reclaims memory on a background thread; it isn’t covered in depth here, but it’s worth knowing it exists for very large keys.)

Redis’s single-threaded execution also means each individual DEL, EXISTS, or TYPE call is atomic — nothing else can run partway through it. What is not atomic is a sequence of separate commands: calling EXISTS and then, in a later round trip, calling DEL only if it existed, leaves a window where another client could delete or recreate the key in between. That distinction comes up again in Common Mistakes.

Expiration interacts with all three commands. If a key has a TTL set (via EXPIRE or SET ... EX) and that TTL has passed, Redis treats the key as gone even if it hasn’t been physically removed yet — this is "lazy expiration." EXISTS, TYPE, and DEL all check the expiration timestamp before doing anything else, so an expired key behaves as if DEL had already run on it: EXISTS returns 0, and TYPE returns none.

Syntax

All three commands operate on key names only — none of them take a value argument.

DEL key [key ...]
EXISTS key [key ...]
TYPE key
Command Arguments Return value Time complexity
DEL One or more key names Integer — number of keys actually removed (nonexistent keys aren’t counted) O(N) overall; O(1) per string key, O(M) per collection key where M is its element count
EXISTS One or more key names Integer — how many of the given names currently exist (a repeated name is counted each time) O(1) per key, O(N) for N keys
TYPE Exactly one key name Simple string: string, list, hash, set, zset, stream, or none if the key doesn’t exist O(1)

Examples

Example 1: Checking, typing, and deleting a single key

SET user:1001:name "Ada"
EXISTS user:1001:name
TYPE user:1001:name
DEL user:1001:name
EXISTS user:1001:name
TYPE user:1001:name

Output:

OK
(integer) 1
string
(integer) 1
(integer) 0
none

The first EXISTS returns 1 because the key is present, and TYPE confirms it’s a plain string. DEL returns 1, confirming exactly one key was removed. After that, EXISTS correctly reports 0, and TYPE on a missing key doesn’t error — it returns the literal string none.

Example 2: TYPE across different structures, and variadic EXISTS/DEL

SET session:abc123 "active"
LPUSH queue:emails "a@example.com" "b@example.com"
HSET user:1002:profile name "Grace" age "36"
SADD tags:post:55 "redis" "database" "cache"
ZADD leaderboard:global 100 "player1" 200 "player2"
TYPE session:abc123
TYPE queue:emails
TYPE user:1002:profile
TYPE tags:post:55
TYPE leaderboard:global
EXISTS session:abc123 queue:emails nonexistent:key
DEL session:abc123 queue:emails
EXISTS session:abc123 queue:emails

Output:

OK
(integer) 2
(integer) 2
(integer) 3
(integer) 2
string
list
hash
set
zset
(integer) 2
(integer) 2
(integer) 0

Five different keys, five different types — TYPE reports each one correctly without needing to know in advance what was stored there. The EXISTS call checks three key names at once and returns 2, because two of the three exist (nonexistent:key doesn’t contribute). DEL then removes both real keys in a single round trip and reports 2 deleted; the final EXISTS confirms both are gone.

Example 3: EXISTS counts repeated names, DEL ignores missing ones

SET cache:page:home "rendered-html"
EXISTS cache:page:home cache:page:home cache:page:home
DEL cache:page:home cache:page:missing
EXISTS cache:page:home

Output:

OK
(integer) 3
(integer) 1
(integer) 0

This is a subtle but important detail: EXISTS is not a boolean. Passing the same key name three times returns 3, not 1, because Redis counts each occurrence independently. The following DEL call names two keys — one real, one never set — and returns 1, because only one of them actually existed to be removed. Deleting a key that isn’t there is not an error; it simply doesn’t count toward the total.

How It Works Step by Step

When the server receives EXISTS user:1001:name, it:

  1. Hashes the key name and looks it up in the database’s main dictionary.
  2. If no entry is found, counts 0 for that key.
  3. If an entry is found, checks whether it has a TTL and, if so, whether that TTL has already passed — an expired-but-not-yet-swept key is treated as absent.
  4. If the key is present and unexpired, adds 1 to the running total and repeats for the next key name given.

When the server receives DEL key1 key2, it performs the same existence-and-expiration check for each name, and for every key genuinely present:

  1. Removes the entry from the main dictionary (and from the expiration dictionary, if it had a TTL).
  2. Frees the memory the value occupied — an O(1) pointer free for a string, or an O(M) walk-and-free for a list, hash, set, sorted set, or stream with M elements.
  3. Increments the count of successfully removed keys.

Because all of this happens on Redis’s single command-processing thread, a multi-key DEL completes as one atomic unit from every other client’s point of view — no other command can observe a state where only some of the keys have been removed.

TYPE key is the simplest of the three: one dictionary lookup, one read of the object’s type tag, and a reply — no traversal of the value itself, regardless of how large it is.

Common Mistakes

Mistake 1: Assuming DEL accepts a glob pattern. Unlike shell rm, DEL only accepts literal key names — it never expands wildcards.

SET user:1:name "Ada"
SET user:2:name "Grace"
DEL user:*
EXISTS user:1:name
EXISTS user:2:name

Output:

OK
OK
(integer) 0
(integer) 1
(integer) 1

DEL user:* looks for a key literally named user:*, finds none, and deletes nothing — both real keys are untouched. To delete keys by pattern, first discover the matching names with SCAN (never KEYS in production — see below), then pass those exact names to DEL.

Mistake 2: Running a type-specific command against a key of the wrong type. Every key has exactly one type for its whole lifetime; a command built for one type errors out against a key of a different type instead of silently doing something unexpected.

SET counter:visits "10"
LPUSH counter:visits "a"

Output:

OK
(error) WRONGTYPE Operation against a key holding the wrong kind of value

counter:visits was created as a string by SET, so LPUSH — which only works on lists — refuses to touch it. Check TYPE first, or better, choose a distinct key namespace per data type (e.g. counter:visits vs. queue:visits) so two access patterns never collide on one key name.

Mistake 3: Using KEYS * (or any broad KEYS pattern) to find keys before deleting them. KEYS is O(N) over the entire keyspace and, because Redis is single-threaded, it blocks every other client for as long as the scan takes — on a production database with millions of keys that can mean multi-second stalls. Use SCAN instead: it walks the keyspace incrementally with a cursor, doing a small bounded amount of work per call, so it never blocks the server for more than a fraction of a millisecond at a time.

Mistake 4: Treating a check-then-act sequence as atomic. Calling EXISTS in one round trip and then conditionally calling DEL in a second round trip is not atomic — another client can create, modify, or delete the key in the gap between the two calls, even though each individual command is atomic on its own. If a race-free "delete only if a condition holds" is required, do it with a single command or a Lua script (EVAL) rather than two separate round trips.

Mistake 5: Forgetting that EXISTS with repeated key names is a count, not a boolean. Code that checks EXISTS key == 1 will misbehave the moment someone changes the call to check several keys at once — always read the integer as a count of how many of the given names exist, not as true/false.

Best Practices

  • Batch multiple keys into a single DEL or EXISTS call instead of looping one key per round trip — it cuts network round trips and is exactly as atomic either way.
  • Use SCAN (with MATCH and a reasonable COUNT) to discover keys by pattern, then feed the results to DEL — never use KEYS against a production dataset.
  • Namespace keys by both purpose and type (session:abc123, queue:emails, leaderboard:global) so a WRONGTYPE error is a clear signal of a bug, not a routine occurrence.
  • Check TYPE defensively when writing code that accepts a key name as configuration or user input, since you can’t guarantee what another part of the system stored there.
  • Remember DEL on a very large collection isn’t free — it’s O(M) for that key’s elements — so deleting huge keys on a latency-sensitive path can cause a visible stall; consider UNLINK for asynchronous reclamation of very large values.
  • Don’t rely on a separate EXISTS check immediately before a write as a substitute for atomic conditional commands like SET key value NX — the check and the write aren’t one atomic step.

Practice Exercises

  1. Create three keys representing a user’s session data: a string session:xyz789:user, a hash session:xyz789:meta, and a set session:xyz789:roles. Use TYPE to confirm each one’s type, then remove all three with a single DEL call and confirm with one EXISTS call (checking all three together) that it returns 0.
  2. Set a key report:2026:daily to any string value. Without deleting it, predict and then check what EXISTS report:2026:daily report:2026:daily report:2026:weekly returns, and explain in your own words why the number is what it is.
  3. Create a list key with RPUSH and then try to run a hash command (such as HGET) against that same key name. Confirm you get a WRONGTYPE error, then use TYPE on the key to see how you could have detected the mismatch before attempting the command.

Summary

  • DEL key [key ...] removes one or more keys and returns how many were actually deleted; nonexistent keys don’t count toward the total and don’t cause an error.
  • EXISTS key [key ...] returns an integer count of how many of the given names currently exist — repeated names are counted repeatedly, so it’s not a boolean.
  • TYPE key returns the key’s data type as a simple string (string, list, hash, set, zset, stream) or none if the key doesn’t exist — it never errors on a missing key.
  • Every key has exactly one type for its lifetime; commands for the wrong type return a WRONGTYPE error rather than silently coercing.
  • EXISTS and TYPE are O(1) per key regardless of value size; DEL is O(1) per string key but O(M) per collection key, where M is its element count.
  • Use SCAN, never KEYS, to find keys by pattern in production, since KEYS blocks the single-threaded server for the whole scan.
  • Separate check-then-act round trips (like EXISTS then DEL) are not atomic as a pair, even though each command is atomic individually.