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
MGETover N sequentialGETcalls 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
MSETto 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
MSETNXspecifically when you need \”initialize this whole group of keys exactly once\” semantics, such as bootstrapping a new session’s fields. - Never assume an
MGETresult ofnilmeans \”key never existed\” — it also means \”key exists but holds the wrong data type.\” UseTYPEorEXISTSseparately if that distinction matters to your application logic. - If a batch needs a shared expiration, don’t rely on
MSETfor it — follow up with individualEXPIREcalls, or pipelineSET key value EX secondscommands instead of usingMSET. - For very large batches (thousands of keys), consider chunking the
MSET/MGETargument list into smaller groups so one oversized command doesn’t monopolize the single-threaded server for an unusually long stretch.
Practice Exercises
- Use one
MSETcall to store three fields forarticle:3001:title,author, andviews(startviewsat\"0\"). Then use oneMGETcall to read all three back at once. - Set a TTL of 120 seconds on
article:3001:titleusingEXPIRE, confirm it withTTL, then overwrite that same key withMSETalongside another key. CheckTTLagain and predict the result before you run it. - Try to use
MSETNXto create two new keys,invite:code:9001andinvite:code:9002, then immediately try anotherMSETNXthat reusesinvite:code:9001plus a brand-new keyinvite:code:9003. Predict the two return values and the final state of all three keys withMGETbefore 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, returningnilfor any key that is missing or holds a non-string value — it never raisesWRONGTYPE.MSETNXwrites its keys only if none of them already exist; if even one exists, nothing is written and it returns0, otherwise it writes everything and returns1.MSETclears any existing TTL on the keys it overwrites, just like a plainSET, and has noKEEPTTLoption.- Batch these commands whenever you’re reading or writing several related keys together — it trades N network round trips for one.
