Chapter 03 · Strings, Counters, Bitmaps, Bitfields, and Binary Values
MGET/MSET, GETRANGE/SETRANGE, APPEND, Binary Payloads, and Network/Memory Tradeoffs
Work with multiple strings, byte ranges, append-only values, and arbitrary binary payloads while measuring response size, sparse allocation, key size, and Cluster multi-key boundaries.
Learning outcomes
AtlasMart now wants to fetch several small configuration values in one request, patch fixed-width bytes inside a compact blob, append device fragments, and store a binary thumbnail fingerprint. A “String” mental model based only on human-readable text becomes actively misleading here. Large multi-key replies, sparse offsets, base64 wrappers, and cross-slot commands can change both correctness and cost.
Use MGET/MSET with a precise model of atomic server execution, nil results, overwrite behavior, O(N) work, response size, and Cluster same-slot constraints.
Use GETRANGE and SETRANGE as byte-range operations with inclusive offsets, negative GETRANGE indexing, zero padding, and 512-MiB String limits.
Use APPEND for deliberate append-only byte sequences while recognizing big-key growth, rewrite/copy cost, and lack of automatic trimming.
Round-trip arbitrary binary bytes without accidental text decoding and measure RESP framing/payload overhead directly.
Compare raw binary storage with base64, sparse allocation, multiple keys, and application chunking using memory/network/persistence/operational evidence.
All Chapter 03 labs reuse the disposable Chapter 01
environment: Redis Open Source 8.10.1 from
Docker Official Image redis:8.10.1, container
atlasmart-redis-ch01, standalone topology, host
publication 127.0.0.1:6379, TLS disabled only
because traffic stays on loopback, default user disabled,
named ACL users atlasmart-app and
academy-admin, logical database 0, AOF with
appendfsync everysec plus RDB snapshots,
persistent /data volume, and no explicit Redis
maxmemory limit/eviction policy. The application
ACL is restricted to ~atlasmart:* and normal
read/write/connection categories. Search, JSON, vector,
time-series, and probabilistic features are not required.
Python examples use only the standard library for byte/RESP
inspection, so no third-party client package is required.
redis-cli remains the primary command client.
1. MGET/MSET reduce command count but do not remove data-model cost
MGET returns one result position for each requested
key. Missing keys and keys that do not hold Strings produce null
entries rather than failing the entire command.
MSET atomically creates or replaces multiple String
values on a single Redis instance. Both are O(N) in the number
of keys, and reply/request bytes still cross the network.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MSET atlasmart:ch03:m:a alpha atlasmart:ch03:m:b beta atlasmart:ch03:m:c gammadocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MGET atlasmart:ch03:m:a atlasmart:ch03:m:missing atlasmart:ch03:m:c# Expected positions: "alpha", (nil), "gamma".# MGET preserves argument order.
On a standalone server, keys can be unrelated. Redis Cluster partitions keys across hash slots, and native multi-key operations generally require appropriate slot locality. A cluster-aware client may split some logical operations into several server commands, which changes atomicity and latency. Chapter 21 will test that explicitly. If AtlasMart knows a set of keys must participate in native same-slot operations, a deliberate Cluster hash tag can preserve locality; do not tag everything into one hot slot.
2. Multi-key replacement can also change TTL state
MSET is a replacement operation. If you need per-key expiration preservation, MSET offers no KEEPTTL option. Keep lifecycle behavior visible in tests instead of treating batching as a transparent transport optimization.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:m:ttl old EX 120docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:m:ttldocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MSET atlasmart:ch03:m:ttl new atlasmart:ch03:m:other valuedocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:m:ttl# Expected lifecycle: the replaced key is persistent (-1) after MSET.
If a service requires “set several values and preserve independent old deadlines,” model that explicitly; do not assume MSET inherits SET's KEEPTTL option. Later transaction/function chapters cover multi-command atomic logic when one command does not express the required invariant.
3. GETRANGE/SETRANGE operate on byte offsets
GETRANGE key start end returns bytes from inclusive
zero-based offsets. Negative GETRANGE offsets count backward
from the end. SETRANGE key offset value overwrites
bytes starting at a non-negative offset and returns the
resulting String length. If the offset lies beyond the current
end, Redis fills the gap with zero bytes.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:blob:fixed ABCDEFGHIJdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GETRANGE atlasmart:ch03:blob:fixed 2 5docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GETRANGE atlasmart:ch03:blob:fixed -3 -1docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SETRANGE atlasmart:ch03:blob:fixed 3 xyzdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:blob:fixeddocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app STRLEN atlasmart:ch03:blob:fixed# GETRANGE 2..5 -> "CDEF"# negative range -> "HIJ" before modification# replacement yields "ABCxyzGHIJ"
Current Redis documentation limits SETRANGE offsets to
2^29 - 1 because a Redis String is limited to 512
MiB. Addressing a far byte on a short String forces Redis to
allocate and zero-fill the intermediate region, which can block
the server. This is the same sparse-growth mechanism you will
see with bitmaps.
4. APPEND is simple, but unbounded append creates a big key
APPEND adds bytes to the existing String and
returns its new byte length. On an absent key it behaves as if
appending to an empty String. Small appends are amortized O(1),
but the resulting value still grows, moves through
persistence/replication, consumes memory, and eventually becomes
expensive to read, copy, back up, or fail over.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app UNLINK atlasmart:ch03:samples:1docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app APPEND atlasmart:ch03:samples:1 0043docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app APPEND atlasmart:ch03:samples:1 0035docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app STRLEN atlasmart:ch03:samples:1docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GETRANGE atlasmart:ch03:samples:1 0 3docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GETRANGE atlasmart:ch03:samples:1 4 7# Length -> 8, ranges -> "0043" and "0035".
For an indefinite event stream, Redis Streams or another log-oriented design is usually a clearer model. APPEND can be efficient for fixed-format bounded segments, but the application must define rotation/retention because String commands do not provide a general “trim arbitrary bytes from the front” operation.
5. Binary-safe round trip: prove bytes, do not trust terminal rendering
The following standard-library Python script speaks RESP2
directly over TCP. It authenticates as the Chapter 01
application ACL user, sends a payload containing NUL and 0xFF
bytes, reads it back, and compares exact bytes. It also prints
the encoded SET request size so payload versus protocol framing
is visible. The script is cross-platform on any host with Python
3 and access to 127.0.0.1:6379.
import socketHOST, PORT = "127.0.0.1", 6379USER = "atlasmart-app"PASSWORD = "AtlasMart-App-Lab-Only-2026"KEY = b"atlasmart:ch03:binary:payload"PAYLOAD = bytes([0x00, 0xFF, 0x41, 0x00, 0x0A, 0x7F])def resp(*parts: bytes) -> bytes: out = [f"*{len(parts)}\r\n".encode()] for p in parts: out += [f"${len(p)}\r\n".encode(), p, b"\r\n"] return b"".join(out)def read_reply(f): prefix = f.read(1) line = f.readline()[:-2] if prefix in (b"+", b"-"): return prefix, line if prefix == b":": return prefix, int(line) if prefix == b"$": n = int(line) if n == -1: return prefix, None data = f.read(n); assert f.read(2) == b"\r\n" return prefix, data raise RuntimeError((prefix, line))with socket.create_connection((HOST, PORT), timeout=3) as s: f = s.makefile("rb") s.sendall(resp(b"AUTH", USER.encode(), PASSWORD.encode())) print("AUTH", read_reply(f)) request = resp(b"SET", KEY, PAYLOAD) print("payload_bytes", len(PAYLOAD)) print("set_request_bytes", len(request)) s.sendall(request); print("SET", read_reply(f)) s.sendall(resp(b"GET", KEY)) _, returned = read_reply(f) print("returned_hex", returned.hex()) print("exact_round_trip", returned == PAYLOAD)# Expected final line: exact_round_trip True.
This test avoids base64 entirely. Base64 is useful when a text-only transport requires it, but encoding 3 input bytes into 4 output characters adds about one-third payload overhead before key metadata, allocator overhead, RESP framing, persistence, or replication. Do not pay that cost automatically when both Redis and the client library can carry raw bytes.
6. Network and memory tradeoffs are shape-dependent
One MGET saves command round trips compared with N sequential
GET commands, but it can create one large reply and one larger
atomic server command. Chapter 15 will measure pipelining
separately. For now, record three sizes: key-name bytes, value
bytes, and RESP framing bytes. Then compare Redis
MEMORY USAGE for representative keys. Memory usage
is not equal to payload length because Redis object/key metadata
and allocator rounding matter.
| Design | Benefit | Cost/risk |
|---|---|---|
| One bounded binary String | Compact metadata, direct range access | Big-key operations and coarse lifecycle if it grows too far. |
| Many small keys | Independent TTL/ownership/sharding | Per-key metadata/cardinality and discovery/persistence overhead. |
| Base64 String | Safe through text-only intermediaries | ~33% payload expansion plus encode/decode CPU. |
| Sparse SETRANGE | O(1)-style indexed addressing after allocation | Zero-filled gap consumes real memory and first growth can block. |
| MGET/MSET | Fewer command exchanges, atomic server command on one instance | O(N) work, large reply/request, Cluster slot restrictions. |
7. Deliberately wrong approach: base64 everything and SETRANGE a distant byte
Two common “safe” shortcuts can be expensive. First, a service base64-encodes every binary value even though its Redis client already supports bytes. Second, an engineer models a sparse identifier space with a single String and writes one byte at offset 8,388,607. That offset is deliberately bounded for the lab: it grows the String to roughly 8 MiB, large enough to demonstrate the mechanism without approaching Redis's 512-MiB limit.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app UNLINK atlasmart:ch03:wrong:sparsedocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin MEMORY USAGE atlasmart:ch03:wrong:sparsedocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SETRANGE atlasmart:ch03:wrong:sparse 8388607 Xdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app STRLEN atlasmart:ch03:wrong:sparsedocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin MEMORY USAGE atlasmart:ch03:wrong:sparse# STRLEN becomes 8,388,608 bytes.# MEMORY USAGE should jump accordingly; exact allocator bytes are version/build dependent.docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app UNLINK atlasmart:ch03:wrong:sparse
The repair is to choose a data structure whose density matches the address space: chunk by meaningful ranges, use separate bounded keys, or choose a Set/Hash/bitmap/other structure based on operations and cardinality. Do not use a sparse String merely because random byte access exists.
8. Hands-on lab: compact product-state transport
Create three small product-state Strings with MSET, read them with MGET, patch one fixed-width region, append a bounded audit suffix, and inspect payload/memory. Keep the values tiny; the goal is semantics, not throughput.
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app UNLINK atlasmart:ch03:l3:p1 atlasmart:ch03:l3:p2 atlasmart:ch03:l3:p3docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MSET atlasmart:ch03:l3:p1 ABCD0001 atlasmart:ch03:l3:p2 ABCD0002 atlasmart:ch03:l3:p3 ABCD0003docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app MGET atlasmart:ch03:l3:p1 atlasmart:ch03:l3:p2 atlasmart:ch03:l3:p3docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SETRANGE atlasmart:ch03:l3:p2 4 9999docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app APPEND atlasmart:ch03:l3:p2 -okdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:l3:p2docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app STRLEN atlasmart:ch03:l3:p2docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GETRANGE atlasmart:ch03:l3:p2 4 7docker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin MEMORY USAGE atlasmart:ch03:l3:p2# Cleanup only this lesson.docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app UNLINK atlasmart:ch03:l3:p1 atlasmart:ch03:l3:p2 atlasmart:ch03:l3:p3 atlasmart:ch03:m:a atlasmart:ch03:m:b atlasmart:ch03:m:c atlasmart:ch03:m:ttl atlasmart:ch03:m:other atlasmart:ch03:blob:fixed atlasmart:ch03:samples:1 atlasmart:ch03:binary:payload
Verification checklist:
- You can explain MGET null positions, MSET atomic replacement on standalone Redis, and the Cluster same-slot caveat.
- You can prove GETRANGE endpoints are inclusive and SETRANGE zero-fills gaps.
- You can state the 512-MiB String / 2^29−1 SETRANGE offset boundary and why far writes can block during allocation.
- You can round-trip NUL/0xFF bytes without text decoding and calculate RESP request bytes versus payload bytes.
- You measured MEMORY USAGE instead of equating logical payload length with total Redis memory.
9. Production judgment
Choose MGET/MSET for bounded related keys when atomic server execution and key locality fit the workload; do not turn them into unbounded “fetch everything” commands. Bound String sizes and range offsets. For binary data, prefer raw bytes through a binary-safe client unless an external text-only boundary justifies encoding. Observe request/reply sizes, network round-trip time, server command latency, key/value sizes, memory usage, AOF/replication volume, persistence rewrite behavior, and hot-key distribution.
Managed services may impose smaller item/command/request limits than Redis Open Source or may alter multi-key behavior through proxy/Active-Active architectures; verify provider documentation. Cluster migration requires reviewing every MGET/MSET/BITOP-style multi-key operation for slot locality. Security review should consider whether raw binary content contains secrets/PII and whether backups/AOF files need encryption and retention controls.
10. Summary and next step
Redis Strings are byte arrays with String commands layered on top. MGET/MSET batch multiple keys but still have O(N), response-size, lifecycle, and Cluster boundaries. GETRANGE/SETRANGE address bytes; APPEND grows values; raw binary can round-trip without base64. Sparse offsets and big Strings create real memory and tail-latency risks. Next, you will use those same bytes as a dense array of individual bits and compare the huge efficiency gain of dense boolean state with the allocation cost of sparse offsets.
Check your understanding
- What does MGET return for a missing key?
- Why can SETRANGE offset 8,388,607 be expensive on a tiny key?
- Are GETRANGE offsets byte or character indexes?
- Why is base64 not automatically required for Redis binary values?
- What changes for MSET in Redis Cluster?
Review the answers
1. A null entry in the corresponding result position; it does not fail the whole MGET.
2. Redis must allocate and zero-fill all intermediate bytes, growing the String to 8 MiB.
3. Byte offsets; String values are binary-safe, not Unicode-character arrays.
4. Redis Strings and binary-capable clients already carry arbitrary bytes; base64 is only needed for text-only boundaries and adds size/CPU overhead.
5. Native multi-key operations are constrained by hash-slot locality; a client may need same-slot keys or split work, changing atomicity/latency.
Authoritative references
- MGET command — multi-key return semantics and complexity
- MSET command — atomic multi-key String replacement
- Multi-key operations — standalone versus Cluster/Active-Active boundaries
- GETRANGE command — inclusive and negative byte-range reads
- SETRANGE command — zero padding, maximum offset, and allocation warning
- APPEND command — append semantics and amortized complexity