Redis Strings, Counters, and Atomic Operations
Strings are the simplest Redis type and the one people underestimate. Beyond storing a value, they give you atomic counters, which solve a problem that's genuinely hard to get right anywhere else: incrementing a shared number from many clients at once without losing updates. Once you see how Redis's atomic operations work, a whole class of features (view counts, rate limits, unique-ID generation, feature counters) becomes a one-line command instead of a careful, race-prone transaction.
This is part 3 of the Redis Masterclass, following the data structures overview.
Strings hold more than text
A Redis string is a binary-safe value up to 512MB. It can hold text, a serialized object, a number, or raw bytes. The basic commands are what you'd expect:
SET user:1:email "aman@example.com"
GET user:1:email
SET config:theme "dark" EX 3600 # with a 1-hour expiry
The key-naming convention above (object-type:id:field) is worth adopting from the start. Colon-separated, hierarchical keys keep your keyspace organized and readable, and they make it easy to reason about what's stored where. Redis doesn't enforce it, but every team that skips it regrets the mess later.
Atomic counters: the killer feature
Here's the problem atomic counters solve. To increment a view count without Redis, you read the current value, add one, and write it back. Between the read and the write, another request does the same, and one increment is lost. This is the classic lost-update race, and under load it happens constantly.
Redis INCR does the whole read-add-write as one atomic operation, so no update is ever lost, no matter how many clients hit it at once:
INCR page:home:views # atomically +1, returns the new value
INCRBY downloads:app 5 # atomically +5
DECR stock:item:1 # atomically -1
INCRBYFLOAT account:1 9.99 # floats too
Every one of these is atomic. A thousand concurrent INCR commands on the same key produce exactly a thousand increments. That guarantee, provided for free, is why Redis is the default home for counters: view counts, likes, download tallies, active-user counts, anything many clients increment.
Counters with expiry: rate limiting in two commands
Combine a counter with an expiry and you have the core of a rate limiter. The pattern is "increment a per-user, per-window key, and reject if it exceeds the limit":
INCR rate:user:42:minute
EXPIRE rate:user:42:minute 60 # only set the TTL on first creation
The first request in a window creates the key and sets a 60-second expiry; subsequent requests increment it; after 60 seconds the key vanishes and the window resets. If the count exceeds your limit, you reject the request. We'll build a complete, race-free rate limiter later in the series, but the foundation is exactly this: an atomic counter that expires. There's a subtlety (setting the expiry only on creation) that Lua scripting solves cleanly, which is why rate limiting gets its own article.
SET with options: locks and conditional writes
The SET command has options that turn it into more than storage. NX means "set only if the key doesn't exist," and XX means "only if it does." Combined with an expiry, SET key value NX EX seconds is the basis of a distributed lock:
SET lock:resource:1 "owner-id" NX EX 30
# returns OK if we got the lock, nil if someone else holds it
Because the set-if-absent-and-expire is atomic, only one client can acquire the lock, and it auto-releases after 30 seconds if the holder crashes. This one command is the seed of the distributed-locking article later on. GETSET and GETDEL (get the old value while setting or deleting) round out the atomic string operations useful for swapping values without a race.
Bitmaps: counters at the bit level
Strings double as bitmaps: you can set and read individual bits, which is a compact way to track boolean flags across a large population. Daily active users is the classic example, one bit per user id per day:
SETBIT dau:2024-04-28 42 1 # user 42 was active today
BITCOUNT dau:2024-04-28 # how many users active today
A million users fit in 125KB, one bit each, and BITCOUNT totals them instantly. For high-volume boolean tracking (feature usage, presence, opt-ins) bitmaps are dramatically more memory-efficient than a set of ids.
The through-line: atomicity
What ties strings together is atomicity. INCR, SET NX, GETDEL, SETBIT: each does its whole operation as one indivisible step, so concurrent clients never corrupt the result. That's the property that makes Redis counters and flags reliable under the exact load where a database read-modify-write would drop updates. When you need many clients to safely touch the same value, a Redis atomic operation is usually the cleanest answer.
Strings look basic, and they are, but the atomic operations layered on them cover counters, rate-limit foundations, locks, and compact flag tracking. Reach for INCR instead of a read-modify-write any time many clients update a shared number, and you get correctness and speed together.
Next, we move to hashes, which store objects with independently-updatable fields, the right structure when a single value isn't enough.
Key takeaways
- Redis strings hold up to 512MB of text, numbers, serialized objects, or binary data; use hierarchical `object:id:field` key names.
- `INCR` and friends do read-add-write atomically, so concurrent clients never lose an increment, which is hard to guarantee against a database.
- An atomic counter plus an expiry is the foundation of rate limiting: increment a per-window key and reject over the limit.
- `SET key value NX EX seconds` atomically sets-if-absent with a TTL, the seed of a distributed lock.
- Bitmaps on strings track boolean flags across large populations in tiny memory, ideal for daily-active-user style tracking.
Frequently asked questions
How do I safely increment a counter in Redis?
Use `INCR` (or `INCRBY`), which performs the read, add, and write as one atomic operation. No matter how many clients increment the same key simultaneously, every increment counts, avoiding the lost-update race of a manual read-modify-write.
Can a Redis string store more than text?
Yes. A string is binary-safe and can hold text, numbers, serialized JSON, or raw bytes, up to 512MB. Redis also treats strings as bitmaps for bit-level operations.
How does Redis help with rate limiting?
An atomic counter with an expiry is the core: increment a per-user, per-window key and set a TTL so the window resets automatically. If the count exceeds the limit, reject the request. Lua scripting makes the expiry-on-creation fully race-free.
What does SET with NX do?
`SET key value NX` sets the key only if it doesn't already exist, returning nil otherwise. Combined with `EX` for expiry, it atomically acquires a key-based lock that only one client can hold and that auto-expires, forming the basis of distributed locks.
What are Redis bitmaps good for?
Compactly tracking boolean flags across a large population, like daily active users with one bit per user id. A million flags fit in about 125KB, and `BITCOUNT` totals the set bits instantly.

About the author
I'm Aman Kumar Singh, a software engineer in Noida, India building scalable full-stack products with React, Next.js, Node.js, NestJS, PostgreSQL, Redis, and AWS. I write about backend engineering, distributed systems, and system design.