Strings, Bitmaps & Bitfields

This content was generated with the assistance of AI and should be verified against the official Redis documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Every example on this page targets Redis Open Source 8.10 (8.10.2), using Jedis 8 for Java examples and Lettuce 7.7 where an async/reactive client is shown. The string is Redis’s most basic data type but also its most feature-dense: a single binary-safe byte sequence that doubles as a JSON blob, a cache entry, a counter, or a raw bit array, depending only on which commands are used against it.

Strings

A Redis string is a binary-safe sequence of bytes, up to 512 MB, so it can hold anything from a short counter to a serialized JSON document or an image. It has no internal structure of its own; structure is imposed by the commands used to read and write it (INCR for a counter, SETBIT/BITFIELD for a bit array, plain GET/SET for a blob).

SET and its flags

SET accepts a mutually exclusive condition group and a mutually exclusive expiration group. The classic condition flags are:

  • NX — only set the key if it does not already exist.

  • XX — only set the key if it already exists.

Redis 8.4 added four further condition flags, IFEQ/IFNE/IFDEQ/IFDNE, which compare against the key’s current value (or a hash digest of it, computed with XXH3) rather than only its existence:

  • IFEQ value — set only if the current value equals value; if the key doesn’t exist, it is not created.

  • IFNE value — set only if the current value is not equal to value; if the key doesn’t exist, it is created.

  • IFDEQ digest / IFDNE digest — the same comparisons, but against a hash digest (as returned by DIGEST) instead of the full value, useful when the value is large and only equality matters.

The expiration group is EX seconds, PX milliseconds, EXAT unix-time-seconds, PXAT unix-time-milliseconds, or KEEPTTL (retain whatever TTL the key already had). GET can be combined with any condition/expiration choice to return the previous value atomically. Prefer this single SET call with NX/XX/IF* over the older, now-deprecated single-purpose commands (SETNX, SETEX, PSETEX, GETSET).

package com.example.bookshop;

import redis.clients.jedis.RedisClient;
import redis.clients.jedis.params.SetParams;

class CatalogueCache {

    private final RedisClient jedis;

    CatalogueCache(RedisClient jedis) {
        this.jedis = jedis;
    }

    void cacheBook(String isbn, String bookJson) {
        // bookshop:cache:book:<isbn> -- catalogue cache entry, expires after 600 seconds
        jedis.set(
                "bookshop:cache:book:" + isbn,
                bookJson,
                new SetParams().ex(600));
    }
}

See SET for the full flag reference.

GETEX and GETDEL

GETEX reads a key’s value while also updating (or clearing, with PERSIST) its TTL in the same round trip — useful for a read that should also refresh a cache entry’s expiration. GETDEL reads a key’s value and deletes it atomically, replacing the older GET + DEL pair.

> GETEX bookshop:cache:book:9780000000001 EX 600
> GETDEL bookshop:cache:book:9780000000001

References: GETEX, GETDEL.

MSET, MSETEX and DELEX

MSET key value [key value …​] writes several keys unconditionally and atomically, with no shared expiration and no per-key options. Redis 8.4 added MSETEX, which extends the same bulk write with the condition and expiration groups SET supports (NX/XX, and EX/PX/EXAT/PXAT/KEEPTTL), applied uniformly to every key in the call:

> MSETEX 2 bookshop:cache:book:9780000000001 <json1> bookshop:cache:book:9780000000002 <json2> NX EX 600

Also new in 8.4 is DELEX, a conditional delete that mirrors SET’s comparison flags: `DELEX key [IFEQ value | IFNE value | IFDEQ digest | IFDNE digest]. Called with no flags it behaves like a plain DEL for a single key; with a flag, the key is only removed if its current value (or digest) matches the condition — useful for releasing a lock or invalidating a cache entry only if it still holds the value the caller last wrote.

References: MSET, MSETEX, DELEX.

Counters: INCR, INCRBY, INCRBYFLOAT and INCREX

INCR/INCRBY increment an integer-typed string by 1 or by a given 64-bit signed amount; INCRBYFLOAT increments a float-typed (or integer-typed) string by a floating-point amount. All three create the key with an initial value of 0 if it doesn’t exist, and are atomic even under concurrent callers.

Redis 8.8 added INCREX, which folds an atomic increment, an optional lower/upper bound (with WRAP-less SATURATE capping instead of failing), and an expiration update into a single command — replacing the INCR + EXPIRE pair that previously needed a Lua script to be atomic. ENX sets the TTL only if the key currently has none, so a fixed window isn’t silently extended by every increment inside it. This is exactly the primitive the bookshop’s rate limiter needs; see Use Cases & Patterns for the full bookshop:ratelimit:<userId> pattern built on INCREX BYINT 1 UBOUND 100 EX 60 ENX.

References: INCR, INCRBY, INCRBYFLOAT, INCREX.

String encodings

Redis picks a string’s internal encoding automatically, visible via OBJECT ENCODING key:

Encoding When used

int

The value is an integer that fits a 64-bit signed range — stored without a separate string allocation.

embstr

A short string, up to the hardcoded OBJ_ENCODING_EMBSTR_SIZE_LIMIT of 44 bytes — the internal sds buffer is allocated in the same chunk as the object header, one allocation instead of two.

raw

Any longer string — a normal, separately allocated sds buffer.

This is purely an internal space optimization; every string command behaves identically regardless of encoding. Reference: OBJECT ENCODING.

Bitmaps

A bitmap is not a separate Redis type — it is a regular string addressed bit by bit. SETBIT key offset value sets a single bit (0 or 1) at a zero-based offset, growing the string as needed; GETBIT key offset reads one back. BITCOUNT counts set bits (optionally over a byte or bit range), and BITPOS finds the first bit set to a given value.

package com.example.bookshop;

import redis.clients.jedis.RedisClient;

class DailyActiveUsers {

    private final RedisClient jedis;

    DailyActiveUsers(RedisClient jedis) {
        this.jedis = jedis;
    }

    void recordActive(String day, long userId) {
        // bookshop:dau:<day> -- one bit per userId, set when that user is seen on that day
        jedis.setbit("bookshop:dau:" + day, userId, true);
    }

    long countActive(String day) {
        return jedis.bitcount("bookshop:dau:" + day);
    }
}

References: SETBIT, GETBIT, BITCOUNT, BITPOS.

BITOP

BITOP combines multiple bitmaps into a destination key. The original operators are AND, OR, XOR (all N-ary) and NOT (unary). Redis 8.2 added four more, which take a distinguished first key X against one or more others:

  • DIFF destkey X Y1 [Y2 …​] — a bit is set if it is set in X but in none of Y1, Y2, …​.

  • DIFF1 destkey X Y1 [Y2 …​] — the inverse: set if it is set in one or more of Y1, Y2, …​ but not in X.

  • ANDOR destkey X Y1 [Y2 …​] — set if it is set in X and in at least one of Y1, Y2, …​.

  • ONE destkey X1 [X2 …​] — set if it is set in exactly one of X1, X2, …​.

ANDOR is a direct fit for a "readers of A who are also interested in at least one of B, C, D" segment against the daily-active-users bitmaps above — e.g. combining a "loves fiction" bitmap with per-genre interest bitmaps to target a promotion. Strings shorter than the longest input (including a missing key) are treated as zero-padded.

Reference: BITOP.

BITFIELD

BITFIELD treats a string as a packed array of arbitrary-width signed (i<bits>) or unsigned (u<bits>) integers, addressed either by a raw bit offset or, prefixed with #, by a field-width-relative index. A single call can chain GET/SET/INCRBY operations, each returning its own reply, with OVERFLOW WRAP|SAT|FAIL controlling how the following SET/INCRBY operations behave on overflow:

  • WRAP (default) — wraps around (modulo arithmetic for unsigned, sign-flip for signed).

  • SAT — saturates at the encoding’s minimum/maximum value.

  • FAIL — performs no write and returns nil for that operation.

package com.example.bookshop;

import java.util.List;
import redis.clients.jedis.RedisClient;

class ShelfSlotCounters {

    private final RedisClient jedis;

    ShelfSlotCounters(RedisClient jedis) {
        this.jedis = jedis;
    }

    long incrementSlotCount(String shelfKey, int slot) {
        // An 8-bit unsigned counter per shelf slot, saturating at 255 instead of wrapping to 0.
        List<Long> result = jedis.bitfield(
                shelfKey,
                "OVERFLOW", "SAT",
                "INCRBY", "u8", "#" + slot, "1");
        return result.get(result.size() - 1);
    }
}

Reference: BITFIELD.

flowchart LR subgraph STRING["bookshop:shelfslots -- one underlying byte string"] direction LR F0["u8 #0\nbits 0-7\nslot 0 count"] F1["u8 #1\nbits 8-15\nslot 1 count"] F2["u8 #2\nbits 16-23\nslot 2 count"] F3["..."] end SETBIT["SETBIT / GETBIT\naddress a single raw bit offset"] -.->|same bytes| STRING BITCOUNT["BITCOUNT / BITPOS\nscan bits across the string"] -.->|same bytes| STRING BITFIELD["BITFIELD GET/SET/INCRBY u8 #N\naddress one field at a time"] -->|writes/reads| F0

BITFIELD lays consecutive fixed-width fields (here, one u8 counter per shelf slot) across the same underlying byte string that SETBIT/GETBIT and BITCOUNT/BITPOS also address bit by bit — #0, #1, #2, …​ select the 0th, 1st, 2nd 8-bit field (bit offsets 0, 8, 16, …​) rather than raw bit offsets passed directly.

  • Getting Started — installing Redis and connecting with Jedis/Lettuce, before running any of the commands above.

  • Use Cases & Patterns — the catalogue cache, cart, and rate-limiter patterns built on the SET, hash, and INCREX commands introduced here.