Valkey bitmaps are not just for setting individual bits; they’re a highly efficient way to represent and query sets of boolean flags or small integers.
Let’s see this in action. Imagine you want to track which users have completed a specific task. Instead of a full Redis set for each user ID, which can be memory-intensive, you can use a single Valkey bitmap.
# Initialize a bitmap with a default size (e.g., 1024 bits)
Valkey> BITFIELD my_task_completion INCRBY signed-int 1 0
(integer) 0
# Mark user 123 as having completed the task (set bit 123 to 1)
Valkey> SETBIT my_task_completion 123 1
(integer) 0
# Mark user 456 as having completed the task
Valkey> SETBIT my_task_completion 456 1
(integer) 0
# Check if user 123 completed the task
Valkey> GETBIT my_task_completion 123
(integer) 1
# Check if user 789 completed the task
Valkey> GETBIT my_task_completion 789
(integer) 0
# Count the number of users who completed the task (up to bit 999)
Valkey> BITCOUNT my_task_task_completion 0 124 # (count up to bit 124, which covers index 123)
(integer) 1
# Count all completed tasks
Valkey> BITCOUNT my_task_completion
(integer) 2
The core problem Valkey bitmaps solve is efficient storage and querying of sparse boolean data. Think of a million users, and only a thousand of them have completed a task. Storing a full SET for each of those thousand users would consume significantly more memory than a single bitmap. A bitmap uses a single bit per flag. For 1,000 flags, that’s 125 bytes (1000 bits / 8 bits/byte). A Redis Set would store the string representation of each user ID, which is much larger.
Internally, Valkey treats a string key as a sequence of bits. When you use SETBIT, GETBIT, or BITCOUNT, Valkey performs bitwise operations on the underlying byte array of the string. Each bit in the string corresponds to a unique position. The index you provide to these commands is the bit offset. For example, bit 0 is the least significant bit of the first byte, bit 7 is the most significant bit of the first byte, bit 8 is the least significant bit of the second byte, and so on.
The primary levers you control are the key (the name of your bitmap) and the index (the specific bit position you’re interested in). Commands like BITCOUNT also allow you to specify a range of bits to count, making it possible to query subsets of your flags.
A common, and often overlooked, aspect of BITCOUNT is its start and end parameters. You can use these to get counts within specific ranges of your bit array without needing to process the entire thing. For instance, if you have daily completion flags for a year, you can easily get the count for just a specific week by providing the appropriate bit offsets derived from the days.
The next challenge when working with bitmaps at scale is often managing multiple related bitmaps efficiently, perhaps for different tasks or user segments.