Chapter 03 · Strings, Counters, Bitmaps, Bitfields, and Binary Values

SET/GET Options, NX/XX, GET, EX/PX, Atomic Initialization, and Conditional Writes

Use Redis strings deliberately: prove SET condition and return-value semantics, preserve or replace TTLs intentionally, and build one-command conditional initialization without hidden races.

Beginner → Intermediate115–145 minutesConditional SET + TTL labRedis Open Source 8.10.1Free/local-firstLast reviewed: September 6, 2026

Learning outcomes

AtlasMart stores checkout idempotency markers, short-lived sessions, feature flags, and small serialized state in Redis strings. A developer currently writes a value with SET, then issues EXPIRE in a second command. Another overwrites a session token and assumes its old time to live survives. A third uses a read-then-write sequence to initialize a key only if it is absent. Each approach can look correct in a single manual test while hiding race conditions or lifecycle changes. This lesson replaces those assumptions with observable String and SET semantics.

01

Explain Redis Strings as binary-safe byte sequences rather than a text-only type, and relate GET/SET replies to Redis Serialization Protocol (RESP) values.

02

Use SET with NX and XX conditions, GET old-value return semantics, and EX/PX expiration options while distinguishing successful writes from rejected conditions.

03

Prove when SET removes an existing TTL, when KEEPTTL preserves it, and why a failed condition must not be treated as a successful initialization.

04

Build atomic one-command initialization with a bounded lifetime instead of a vulnerable check-then-set or SET-then-EXPIRE sequence.

05

Connect conditional writes to client retries, persistence, replication/failover limits, Cluster slot locality, ACL permissions, and observability.

Exact lab baseline

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. This chapter deliberately uses one standalone node so command semantics are isolated from replication/failover. Later chapters re-test the same patterns under replication, Sentinel, and Cluster.

1. A Redis String is a binary-safe value, not a text promise

Redis calls its simplest value type a String, but the server stores a length-delimited sequence of bytes. Those bytes may encode UTF-8 text, JSON, a compressed object, an integer-looking token, a bitmap, or arbitrary binary data. GET returns the stored bytes as a bulk-string reply. Your client decides whether to expose those bytes as text, a byte array, or another host-language representation.

That distinction matters immediately. The bytes 31 30 happen to be the ASCII characters 10; numeric commands can parse them as an integer. The bytes 00 ff 41 are equally valid as a Redis String, but decoding them as UTF-8 would be an application error. Lesson 3 performs an exact byte-for-byte round trip.

redis-cli · smallest observable string state
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:flag:checkout enableddocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:flag:checkoutdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app STRLEN atlasmart:ch03:flag:checkoutdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GETRANGE atlasmart:ch03:flag:checkout 0 2# Expected shape:# OK# "enabled"# (integer) 7# "ena"

STRLEN counts bytes, not Unicode characters. GETRANGE addresses byte offsets and includes both endpoints. Those details are easy to miss when all examples contain plain ASCII.

2. SET replaces the value; conditions decide whether replacement happens

SET key value creates the key when absent and replaces the current value when present, regardless of the previous Redis data type. NX changes the condition to “only if the key does not exist.” XX changes it to “only if the key already exists.” These condition options are mutually exclusive. A successful ordinary SET replies OK; a condition that rejects the write produces a null reply rather than modifying the key.

Form Condition Main use Important boundary
SET k v Always Create or replace Existing TTL is discarded unless an expiration option/KEEPTTL says otherwise.
SET k v NX Key must be absent Atomic initialization / claim-if-absent A null reply means no write occurred; caller must branch explicitly.
SET k v XX Key must exist Update-if-present Does not prove the caller read the same version earlier.
SET k v GET Normal SET condition Return previous String while writing new value If the existing value is not a String, Redis errors and aborts the SET.
redis-cli · prove NX, XX, and GET rather than infer them
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app UNLINK atlasmart:ch03:set:demodocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:set:demo v1 NXdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:set:demo v2 NXdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:set:demodocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:set:demo v2 XX GETdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:set:demo# Expected shape:# first NX -> OK# second NX -> (nil), value remains v1# XX GET -> old value "v1", while the stored value becomes v2

The GET option makes the read and replacement part of one server command; it does not turn XX into compare-and-swap. Redis 8.10.1 also documents additional value/digest conditional SET modes. They are version-sensitive extensions, so this curriculum keeps its main mental model on NX/XX and asks you to re-check current command documentation before relying on newer conditions.

3. EX, PX, and KEEPTTL are lifecycle semantics, not decoration

EX attaches a relative expiration in seconds; PX uses milliseconds. A time to live (TTL) is the remaining interval before Redis considers the key expired. Chapter 02 showed that expiration is not an exact task scheduler. Here the crucial rule is replacement: plain SET discards an existing TTL. KEEPTTL explicitly retains it. An EX/PX option replaces the lifetime with a new deadline.

redis-cli · observe TTL replacement and preservation
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:session:ttl token-v1 EX 90docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:session:ttl# WRONG ASSUMPTION: plain SET preserves the old lifetime.docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:session:ttl token-v2docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:session:ttl# -> -1: key exists and is now persistent.# Attach a new lifetime, then preserve it on replacement.docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:session:ttl token-v3 PX 90000docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app PTTL atlasmart:ch03:session:ttldocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:session:ttl token-v4 XX GET KEEPTTLdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app PTTL atlasmart:ch03:session:ttl# -> positive and decreasing; exact milliseconds depend on observation time.

Do not assert that the two PTTL readings differ by exactly the time spent typing. Client/network scheduling and expiration processing make precise wall-clock output variable. The evidence you need is state class: the plain replacement changed TTL to -1; the KEEPTTL replacement left a positive expiration.

4. Atomic initialization means one command contains the condition and lifetime

Redis executes one command without interleaving another command in the middle of that command. That is the useful scope of command atomicity. If AtlasMart wants to create a short-lived “checkout request seen” marker only when absent, SET ... NX EX ... keeps the absence check, write, and lifetime attachment in one command.

redis-cli · one-command conditional initialization
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app UNLINK atlasmart:ch03:idempotency:req-9001docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:idempotency:req-9001 processing NX EX 120docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:idempotency:req-9001 processing-again NX EX 120docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:idempotency:req-9001docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:idempotency:req-9001# Exactly one initialization succeeds on this single primary.# A retry that sees (nil) must NOT be reported as a fresh successful claim.

This is stronger than EXISTS followed by SET, because another client cannot win between the test and write. It is also stronger than SET NX followed by a separate EXPIRE, because a client crash between those commands cannot leave the newly created marker persistent. It is not, by itself, a complete distributed lock or exactly-once workflow; later chapters address ownership tokens, leases, idempotency, failover, and reconciliation.

5. Deliberately wrong workflow: a session refresh silently becomes immortal

Assume AtlasMart sessions are required to expire. A handler receives a refreshed token and runs plain SET. The value is correct, so a functional test passes, but the old expiration disappears. This is a data-retention and capacity bug, not merely a syntax issue.

redis-cli · reproduce, diagnose, repair, verify
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:wrong:session s1 EX 45docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:wrong:sessiondocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:wrong:session s2docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:wrong:session# -> -1 proves the lifecycle changed.# Repair option A: preserve the existing deadline.docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:wrong:session s3 XX KEEPTTL# The TTL was already lost, so KEEPTTL now preserves "persistent".# Repair option B: explicitly restore policy with a new bounded TTL.docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:wrong:session s4 XX EX 45docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:wrong:session# -> positive <= 45

The first attempted repair is intentionally instructive: KEEPTTL cannot resurrect an expiration that a previous command already removed. Correctness depends on when the policy is applied, not merely on whether the final command contains the right option.

6. What Redis is doing—and what this does not guarantee

The server parses the RESP command, checks ACL permission and key pattern, evaluates the SET condition against current key state, validates the current type if GET needs the old value, then applies the replacement and expiration policy as one command. The command is fast for ordinary small values, but payload size still affects memory allocation, network transfer, persistence/AOF traffic, replication traffic, and tail latency.

On this standalone lab, a successful reply proves the primary executed the command. With AOF everysec, it does not prove that bytes are already fsynced to durable storage. Under asynchronous replication later, it also would not prove every replica has received the write. Client timeouts create ambiguity: the server may have executed a command even if the reply was lost. Conditional writes therefore need retry semantics designed at the application level.

7. Hands-on lab: define a safe AtlasMart initialization contract

Create three keys: a persistent feature flag, an expiring checkout marker, and a session whose TTL must survive a value refresh. Keep all state under atlasmart:ch03:l1:. Record the before/after values and TTL classes so the test can detect lifecycle regressions.

shell · deterministic conditional-write fixture
docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app UNLINK atlasmart:ch03:l1:flag atlasmart:ch03:l1:checkout atlasmart:ch03:l1:sessiondocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:l1:flag off NXdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:l1:checkout accepted NX EX 180docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:l1:session token-a EX 180docker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app SET atlasmart:ch03:l1:session token-b XX GET KEEPTTLdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:l1:flagdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:l1:checkoutdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:l1:checkoutdocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app GET atlasmart:ch03:l1:sessiondocker exec -e REDISCLI_AUTH=AtlasMart-App-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user atlasmart-app TTL atlasmart:ch03:l1:sessiondocker exec -e REDISCLI_AUTH=AtlasMart-Admin-Lab-Only-2026 atlasmart-redis-ch01 redis-cli --user academy-admin MEMORY USAGE atlasmart:ch03:l1:session# 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:l1:flag atlasmart:ch03:l1:checkout atlasmart:ch03:l1:session

Verification checklist:

  • The checkout key is created only on the first NX attempt and has a positive bounded TTL.
  • The session replacement returns the previous String when GET is used and the key still has a positive TTL after KEEPTTL.
  • You can explain why plain SET would remove the session TTL and why KEEPTTL cannot restore a deadline already lost.
  • You can distinguish command atomicity on the current primary from durability, replication acknowledgement, and exactly-once application semantics.
  • You recorded the Redis 8.10.1 / standalone / DB 0 / ACL / AOF-everysec assumptions beside the result.

8. Production judgment

Use a String when the value is naturally one binary blob or scalar and the operations you need match String commands. Use NX/XX when the existence predicate is actually the invariant; do not substitute them for version checks or authorization. Pair creation with EX/PX in the same SET when an absent TTL would be harmful. Use KEEPTTL only when retaining the existing deadline is the explicit contract.

In production, observe request/value sizes, memory growth, command latency distributions, key cardinality, expiration state, errors/null conditional replies, retry counts, AOF/replication pressure, and hot-key concentration. Cluster deployments add same-slot constraints for multi-key logic, while a single SET addresses one key and routes naturally through a cluster-aware client. ACLs control which users may issue the command and which key patterns they can touch; key prefixes alone are not security. For rollback, preserve compatibility with old readers/writers and define what happens if a new deployment starts using KEEPTTL or different TTL values.

9. Summary and next step

Redis Strings are binary-safe values. SET creates or replaces, NX/XX gate that replacement, GET can return the old String as part of the same command, and EX/PX/KEEPTTL define the lifecycle of a successful write. Plain SET removes an existing TTL. One-command conditional initialization removes check/write and write/expire races, but it does not create exactly-once distributed processing. Next, you will use the same String representation as an atomic integer or floating-point counter and confront overflow, precision, retry, and sequencing boundaries.

Check your understanding

  1. Why can a plain SET turn a correctly expiring session into a retention bug?
  2. What does NX make atomic?
  3. Why is XX not compare-and-swap?
  4. What does SET ... GET add?
  5. Why can a timeout after SET be ambiguous?
Review the answers

1. Because replacing a key with SET discards its existing TTL unless a new expiration or KEEPTTL is specified.

2. The existence test and successful value/expiration write are evaluated inside one SET command on the server.

3. XX only requires that the key exists; it does not compare the current value with a version previously read by the client.

4. It returns the previous String value while applying the new value in the same command; a non-String old value causes an error.

5. The server may have executed the command even if the client did not receive the reply, so retries must be designed with application semantics.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.