Why Redis Strings are not strings — they are binary-safe byte arrays. Integer encoding, atomic INCR/DECR, the GET/SET command family, MGET/MSET for batching, and why storing JSON blobs is the first mistake engineers make.
F-2 — Strings, Numbers, and Binary Safety
Who this module is for: You have used
SETandGETand assumed you understood Redis Strings. This module will show you what is actually happening under the hood — how Redis encodes integers differently from text, why "String" is a misleading name for what is really a binary-safe byte array, and why the seemingly obvious pattern ofSET key (JSON.stringify(obj))creates problems you will only discover under load.
What a Redis String Actually Is
The name "String" is the first thing Redis gets slightly wrong. When most engineers hear "string," they think of text — a sequence of Unicode characters with an encoding like UTF-8.
A Redis String is not that.
A Redis String is a binary-safe byte array — a sequence of raw bytes with no imposed encoding, no null-termination requirement, and no character set assumption. It can hold:
- Plain text:
"Hello, Redis" - A serialized JSON object:
"{\"id\":1,\"name\":\"Jatin\"}" - A serialized Protocol Buffer
- A JPEG image
- A packed binary struct
- An integer:
"42" - An empty string:
""
The maximum size is 512 MB per key. In practice, values larger than a few kilobytes start to create problems (large values block the event loop during serialization, consume significant memory, and become expensive to transfer over the network), but the hard limit is 512 MB.
The "binary-safe" property matters because many key-value systems from Redis's era (early 2000s Memcached, for example) used C-style null-terminated strings, which meant you could not store arbitrary binary data — a null byte would terminate the string early. Redis stores the length alongside the data, so a null byte in the middle of a value is perfectly valid.
How Redis Encodes Strings Internally
Here is something most Redis users never learn: Redis does not store all String values the same way internally. It uses three different encodings depending on the value:
int encoding
If the value is an integer that fits in a 64-bit signed long (roughly -9.2 × 10¹⁸ to 9.2 × 10¹⁸), Redis stores the actual integer, not a string representation of it.
127.0.0.1:6379> SET counter 42
OK
127.0.0.1:6379> OBJECT ENCODING counter
"int"
This is significant for two reasons:
-
Memory efficiency. An integer stored as
intencoding takes 8 bytes. The same number stored as a string"42"would take 2 bytes of data plus string header overhead. For large integers like Unix timestamps or IDs, the difference is meaningful at scale. -
Atomic arithmetic.
INCR,DECR,INCRBY,DECRBYonly work on keys withintencoding (or keys whose value is a string representation of an integer). Redis can parse and operate on them atomically.
Additionally, Redis maintains a shared integer pool for the integers 0 through 9999. When you store any of these values, Redis does not allocate new memory — it points to a pre-allocated shared object. This is why OBJECT REFCOUNT on small integers returns a large number.
127.0.0.1:6379> SET x 100
OK
127.0.0.1:6379> OBJECT REFCOUNT x
(integer) 2147483647 # shared object, refcount saturated
embstr encoding
For strings up to 44 bytes, Redis uses embstr (embedded string) encoding. The string header and the data are allocated in a single contiguous memory block, making it cache-friendly and reducing allocator overhead.
127.0.0.1:6379> SET name "Jatin Jain Saraf"
OK
127.0.0.1:6379> OBJECT ENCODING name
"embstr"
embstr objects are immutable — any modification (like APPEND) causes Redis to convert the encoding to raw and reallocate.
raw encoding
For strings longer than 44 bytes, Redis uses raw encoding: a standard dynamic string (SDS — Simple Dynamic String) where the header and data are in separate memory allocations.
127.0.0.1:6379> SET longkey "This string is definitely longer than forty-four bytes total"
OK
127.0.0.1:6379> OBJECT ENCODING longkey
"raw"
Why does this matter? Because encoding determines memory usage and performance characteristics. If you are storing millions of short strings (session tokens, feature flags, user IDs), embstr gives you better cache locality. If you are storing large JSON blobs, raw encoding is unavoidable — and that is where the "storing JSON in a String" pattern starts to cost you.
The SET Command in Full
Most engineers know SET key value. The full signature is considerably richer:
SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]
Let us go through each option:
Expiry options
# Expire in 3600 seconds (1 hour)
SET session:user:1001 "token_abc" EX 3600
# Expire in 3600000 milliseconds (1 hour, more precise)
SET session:user:1001 "token_abc" PX 3600000
# Expire at a specific Unix timestamp (seconds)
SET promo:summer2025 "active" EXAT 1751299200
# Expire at a specific Unix timestamp (milliseconds)
SET promo:summer2025 "active" PXAT 1751299200000
# Preserve the existing TTL (don't reset it on update)
SET session:user:1001 "new_token" KEEPTTL
KEEPTTL is underused. It lets you update a value without accidentally removing the TTL. Without it, SET on a key that already has a TTL will reset the TTL to infinite (persistent) — a common source of session expiry bugs.
Conditional options
# NX — only set if key does NOT exist
SET lock:resource:42 "owner_uuid" NX EX 30
# Returns OK if the key was set, (nil) if it already existed
# XX — only set if key DOES exist
SET user:1001:name "Updated Name" XX
# Returns OK if the key existed and was updated, (nil) if it did not exist
NX is the foundation of distributed locking (covered in depth in A-5). SET key value NX EX seconds is the atomic "acquire a lock" primitive — it either sets the key and returns OK (lock acquired) or returns (nil) (lock already held).
The old pattern of SETNX + EXPIRE as two separate commands is broken — if the process crashes between the two commands, the key has no expiry and the lock is never released. SET ... NX EX is atomic. Always use it.
GET option (Redis 6.2+)
# Set the new value and return the OLD value atomically
SET user:1001:status "offline" GET
# Returns the previous value of user:1001:status (e.g., "online")
# or (nil) if the key did not previously exist
This is equivalent to the old GETSET command (which is now deprecated), but integrated into SET itself.
The GET Command Family
Beyond plain GET, Redis provides several atomic read-and-modify commands:
GETDEL
# Get the value and delete the key atomically
GETDEL session:user:1001
# Returns the value and removes the key in one operation
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion