MSET and MGET

MSET and MGET are the batch versions of SET and GET: MSET writes several key-value pairs in a single command, and MGET reads several keys in a single command, each in one round trip to the server. They exist purely for efficiency — instead of paying network latency for N separate SET or GET calls, you pay it once for all N keys. A close cousin, MSETNX, adds an all-or-nothing atomicity guarantee: it only writes any of the keys if none of them already exist.

Overview / How MSET and MGET Work

Redis is single-threaded: one command runs to completion on the main thread before the next one starts. That means MSET writing five keys happens as five sequential dictionary inserts into the keyspace with no other client’s command able to interleave in the middle — from every other connection’s point of view, either none of the five keys exist yet, or all five do. This is what makes MSET useful for writing a group of related fields (say, several fields that describe one user or one product) without a separate locking mechanism.

MGET works the same way on the read side: it looks up each requested key in the keyspace dictionary, one after another, and assembles the results into a single array reply. Importantly, MGET is forgiving about missing or wrong-typed keys — for any key that does not exist, or that holds a non-string value (a list, hash, set, etc.), MGET returns nil in that position instead of raising an error. This is different from most other commands, which raise a WRONGTYPE error when you call them against the wrong data type.

One detail that trips people up: MSET, like a plain SET, clears any existing TTL on a key it overwrites. There is no KEEPTTL option for MSET the way there is for SET, so if any of the keys you are batch-writing currently have an expiration set, that expiration is silently removed the moment MSET touches them.

MSETNX: Atomic Conditional Batch Writes

MSETNX behaves like MSET except it first checks whether any of the target keys already exist. If even one of them does, MSETNX writes nothing at all and returns 0. Only if none of the keys exist does it write all of them and return 1. This all-or-nothing behavior is itself atomic thanks to Redis’s single-threaded execution model, which makes MSETNX a good fit for bootstrapping a group of keys exactly once — for example, initializing every field of a brand-new session or a brand-new counter set, where you never want a partial write.

Syntax

The general forms, along with the return value and complexity of each command, are:

MSET key value [key value ...]
MGET key [key ...]
MSETNX key value [key value ...]
Command Arguments Returns Time Complexity
MSET One or more key value pairs Always OK (cannot fail on valid syntax) O(N) for N keys
MGET One or more key names An array with one entry per key, in the same order; nil for any key that is missing or holds a non-string value O(N) for N keys
MSETNX One or more key value pairs (integer) 1 if all keys were set, (integer) 0 if none were set because at least one already existed O(N) for N keys

Examples

Example 1: Writing and reading several fields at once

MSET user:1001:name "Ada" user:1001:email "ada@example.com" user:1001:age "36"
MGET user:1001:name user:1001:email user:1001:age

Output:

OK
1) "Ada"
2) "ada@example.com"
3) "36"

The single MSET call writes all three fields for user 1001 in one round trip and always replies OK. The follow-up MGET reads all three back in one call, returning an array whose order matches the order the keys were requested in.

Example 2: MGET with a key that doesn’t exist

SET product:2001:name "Widget"
MGET product:2001:name product:2001:price product:2001:stock

Output:

OK
1) "Widget"
2) (nil)
3) (nil)

Only product:2001:name was ever set. MGET doesn’t error out because two of the three keys are missing — it simply slots (nil) into their positions in the reply array, leaving the caller to check for missing entries themselves.

Example 3: MSETNX and its all-or-nothing guarantee

MSETNX session:abc123:user "42" session:abc123:role "admin"
MSETNX session:abc123:user "99" session:abc123:ip "10.0.0.5"
MGET session:abc123:user session:abc123:role session:abc123:ip

Output:

(integer) 1
(integer) 0
1) "42"
2) "admin"
3) (nil)

The first MSETNX succeeds because neither key existed yet, so it writes both and returns 1. The second MSETNX tries to write session:abc123:user again along with a new key, session:abc123:ip — but because session:abc123:user now already exists, the entire command is rejected and returns 0, leaving session:abc123:ip unset. The final MGET confirms the original value of session:abc123:user is untouched and ip was never written.

How It Works Step by Step

For MSET key1 val1 key2 val2: Redis’s single command-processing thread parses the argument list into pairs, then walks the pairs in order, performing a dictionary insert-or-overwrite for each key exactly as a plain SET would (including removing any existing TTL on that key). Because no other command can run on the main thread in between those inserts, a client issuing MGET concurrently will never observe a state where only some of the MSET pairs have landed.

For MGET key1 key2 key3: Redis walks the key list in order, doing a dictionary lookup for each one. If the lookup finds nothing, or finds an entry whose internal encoding isn’t a string type, that position gets nil. Otherwise the string value is copied into the reply array. The whole operation completes as one atomic step before any other client’s command runs, but since it only reads, that atomicity mainly guarantees a consistent snapshot across the keys rather than preventing conflicts.

For MSETNX, Redis first loops through every key checking existence with no writes yet. If any exists, it stops immediately and returns 0 without touching the keyspace at all. Only if the whole existence check passes does it perform the same sequential dictionary inserts that MSET does, then returns 1.

Common Mistakes

Mistake 1: Assuming MSET preserves an existing TTL

SET promo:code:1001 "SAVE10" EX 3600
TTL promo:code:1001
MSET promo:code:1001 "SAVE20" promo:code:1002 "SAVE30"
TTL promo:code:1001

Output:

OK
(integer) 3600
OK
(integer) -1

promo:code:1001 had a 3600-second TTL, but the moment MSET overwrote it, the TTL was silently cleared — TTL now returns -1 (no expiration). This is a common source of memory leaks: developers expect MSET to behave like an update that leaves metadata alone, but it always behaves like a fresh SET per key. If you need to preserve expirations, reissue EXPIRE on the affected keys immediately after the MSET, or write each key individually with SET key value KEEPTTL instead of batching with MSET.

Mistake 2: Expecting MGET to error on a wrong-type key

RPUSH cart:5001:items "sku1" "sku2"
MGET cart:5001:items

Output:

(integer) 2
1) (nil)

cart:5001:items is a list, not a string, so most string commands (like GET) would raise a WRONGTYPE error against it. MGET does not — it just returns nil for that slot, exactly as it would for a key that doesn’t exist at all. Code that treats every nil from MGET as \”key is missing\” can silently swallow a real bug where the wrong data type ended up under that key name. If type-safety matters, check with TYPE key first or keep string-only keys under a naming convention that can’t collide with other data structures.

Best Practices

  • Prefer MGET over N sequential GET calls whenever you need several known keys — it collapses N network round trips into one, which matters far more for latency than the O(N) server-side cost.
  • Use MSET to write a batch of independent keys atomically, but remember it always succeeds and always clears TTLs on the keys it touches — it is not a conditional or TTL-preserving operation.
  • Reach for MSETNX specifically when you need \”initialize this whole group of keys exactly once\” semantics, such as bootstrapping a new session’s fields.
  • Never assume an MGET result of nil means \”key never existed\” — it also means \”key exists but holds the wrong data type.\” Use TYPE or EXISTS separately if that distinction matters to your application logic.
  • If a batch needs a shared expiration, don’t rely on MSET for it — follow up with individual EXPIRE calls, or pipeline SET key value EX seconds commands instead of using MSET.
  • For very large batches (thousands of keys), consider chunking the MSET/MGET argument list into smaller groups so one oversized command doesn’t monopolize the single-threaded server for an unusually long stretch.

Practice Exercises

  1. Use one MSET call to store three fields for article:3001: title, author, and views (start views at \"0\"). Then use one MGET call to read all three back at once.
  2. Set a TTL of 120 seconds on article:3001:title using EXPIRE, confirm it with TTL, then overwrite that same key with MSET alongside another key. Check TTL again and predict the result before you run it.
  3. Try to use MSETNX to create two new keys, invite:code:9001 and invite:code:9002, then immediately try another MSETNX that reuses invite:code:9001 plus a brand-new key invite:code:9003. Predict the two return values and the final state of all three keys with MGET before checking.

Summary

  • MSET key value [key value ...] writes multiple keys in one atomic, always-successful command; time complexity is O(N) for N keys.
  • MGET key [key ...] reads multiple keys in one command, returning nil for any key that is missing or holds a non-string value — it never raises WRONGTYPE.
  • MSETNX writes its keys only if none of them already exist; if even one exists, nothing is written and it returns 0, otherwise it writes everything and returns 1.
  • MSET clears any existing TTL on the keys it overwrites, just like a plain SET, and has no KEEPTTL option.
  • Batch these commands whenever you’re reading or writing several related keys together — it trades N network round trips for one.