VALKEY-MODULES-BLOCKING-OPS(7)
π VALKEY-MODULES-BLOCKING-OPS(7) π
okay bestie, welcome to the Valkey Manual β¨ this is the one about modules and blocking commands, and ngl it's a whole saga so it's split into parts β this is part 1 π«Ά
πΈ NAME πΈ
valkey-modules-blocking-ops β Modules and blocking commands
β¨ DESCRIPTION β¨
So. Valkey ships with a bunch of blocking commands in the built-in set, right? The most iconic one is BLPOP (and its twin sister BRPOP) which justβ¦ sits there, waiting for elements to show up in a list. Main character behavior tbh. π
The actually interesting part about blocking commands is that they do not block the whole server β they only block the client that called them. That's the whole vibe. π
And usually the reason we're blocking at all is because we're waiting on some external event: could be a change in the Valkey data structures (hi, BLPOP), could be a long computation happening in a thread, could be data arriving from the network, whatever. Many possibilities, one block. π
And guess what β Valkey modules can implement blocking commands too π this documentation shows you how the API works and walks through a few patterns you can use to model blocking commands. Let's gooo.
π¦ How blocking and resuming works π¦
Note: you might wanna peek at thehelloblock.cexample in the Valkey source tree inside thesrc/modulesdirectory β it's a simple-to-understand example of how the blocking API gets applied. go look, i'll wait π
In Valkey modules, commands are implemented by callback functions that the Valkey core invokes when the user calls that specific command. Normally the callback finishes up and sends some reply to the client. But if you use the function below instead, the function implementing the module command can request that the client gets put into the blocked state:
ValkeyModuleBlockedClient *ValkeyModule_BlockClient(ValkeyModuleCtx *ctx,
ValkeyModuleCmdFunc reply_callback,
ValkeyModuleCmdFunc timeout_callback,
void (*free_privdata)(void*),
long long timeout_ms);
This function hands you back a ValkeyModuleBlockedClient object, which you'll use later to unblock the client. Here's what each argument means, bestie:
ctxβ the command execution context, same as everywhere else in the API.reply_callbackβ the callback, with the same prototype as a normal command function, that gets called when the client is unblocked, so it can return a reply to the client.timeout_callbackβ the callback, again with the same prototype as a normal command function, that gets called when the client hits themstimeout.free_privdataβ the callback called in order to free the private data. Private data is a pointer to some data that gets passed between the API used to unblock the client and the callback that will send the reply to the client. We'll see how this mechanism works later in this document. πmsβ the timeout in milliseconds. When the timeout is reached, the timeout callback is called and the client is automatically aborted.
Once a client is blocked, you can unblock it with this API:
int ValkeyModule_UnblockClient(ValkeyModuleBlockedClient *bc, void *privdata);
It takes, as its argument, the blocked client object returned by that earlier call to ValkeyModule_BlockClient(), and unblocks the client. Immediately before the client gets unblocked, the reply_callback function you specified when blocking the client gets called β and that function will have access to the privdata pointer you passed here. β¨
IMPORTANT: that function above is thread safe, and it can be called from within a thread doing some work, in order to implement the command that blocked the client. π₯
The privdata data gets freed automatically using the free_privdata callback when the client is unblocked. This is useful because the reply callback may never be called at all if the client times out or disconnects from the server β so it's important that it's up to an external function to take responsibility for freeing the data passed, if needed. no cap. π
To better understand how the API works, let's imagine writing a command that blocks a client for one second, and then sends Hello! as the reply.
Note: arity checks and other not-important things are not implemented in this command, so the example stays simple. π«Ά
int Example_ValkeyCommand(ValkeyModuleCtx *ctx, ValkeyModuleString **argv,
int argc)
{
ValkeyModuleBlockedClient *bc =
ValkeyModule_BlockClient(ctx,reply_func,timeout_func,NULL,0);
pthread_t tid;
pthread_create(&tid,NULL,threadmain,bc);
return VALKEYMODULE_OK;
}
void *threadmain(void *arg) {
ValkeyModuleBlockedClient *bc = arg;
sleep(1); /* Wait one second and unblock. */
ValkeyModule_UnblockClient(bc,NULL);
}
That command blocks the client ASAP and spawns a thread that waits one second and then unblocks the client. Now let's check out the reply and timeout callbacks β in our case they're super similar, since they just reply to the client with a different reply type. π
int reply_func(ValkeyModuleCtx *ctx, ValkeyModuleString **argv,
int argc)
{
return ValkeyModule_ReplyWithSimpleString(ctx,"Hello!");
}
int timeout_func(ValkeyModuleCtx *ctx, ValkeyModuleString **argv,
int argc)
{
return ValkeyModule_ReplyWithNull(ctx);
}
The reply callback just sends the Hello! string to the client. The important bit here is that the reply callback is called when the client is unblocked from the thread. πΈ
The timeout command returns NULL, as it often happens with actual Valkey blocking commands timing out. it's giving "welp, nothing happened" π
π Passing reply data when unblocking π
That example above is simple to understand but it's missing an important real-world aspect of an actual blocking command implementation: often the reply function needs to know what to reply to the client, and that information is often provided as the client is unblocked. iykyk.
So we could modify the example above so that the thread generates a random number after waiting one second. Think of it as an actually expensive operation of some kind. Then that random number can be passed to the reply function, so we return it to the command caller. To make this work, we modify the functions like this:
void *threadmain(void *arg) {
ValkeyModuleBlockedClient *bc = arg;
sleep(1); /* Wait one second and unblock. */
long *mynumber = ValkeyModule_Alloc(sizeof(long));
*mynumber = rand();
ValkeyModule_UnblockClient(bc,mynumber);
}
See how the unblocking call is now passing some private data β that mynumber pointer β to the reply callback? π And to get hold of that private data, the reply callback uses this function:
void *ValkeyModule_GetBlockedClientPrivateData(ValkeyModuleCtx *ctx);
So our reply callback gets modified like this:
int reply_func(ValkeyModuleCtx *ctx, ValkeyModuleString **argv,
int argc)
{
long *mynumber = ValkeyModule_GetBlockedClientPrivateData(ctx);
/* IMPORTANT: don't free mynumber here, but in the
* free privdata callback. */
return ValkeyModule_ReplyWithLongLong(ctx,mynumber);
}
Also note that we need to pass a free_privdata function when blocking the client with ValkeyModule_BlockClient(), since that allocated long value has to be freed. Our callback will look like this:
void free_privdata(void *privdata) {
ValkeyModule_Free(privdata);
}
NOTE: it's important to stress that the private data is best freed in the free_privdata callback, because the reply function may not be called at all if the client disconnects or times out. π
Also note that the private data is also accessible from the timeout callback, always using the GetBlockedClientPrivateData() API. β¨
π§ Aborting the blocking of a client π§
One problem that sometimes comes up: we need to allocate resources in order to implement the non-blocking command. So we block the client, then, for example, try to create a thread β but the thread creation function returns an error. What do we do in that situation to recover? π₯² We don't want to leave the client blocked, and we also don't want to call UnblockClient() because that will trigger the reply callback to be called.
In this case the best thing to do is use this function:
int ValkeyModule_AbortBlock(ValkeyModuleBlockedClient *bc);
And practically, this is how you use it:
int Example_ValkeyCommand(ValkeyModuleCtx *ctx, ValkeyModuleString **argv,
int argc)
{
ValkeyModuleBlockedClient *bc =
ValkeyModule_BlockClient(ctx,reply_func,timeout_func,NULL,0);
pthread_t tid;
if (pthread_create(&tid,NULL,threadmain,bc) != 0) {
ValkeyModule_AbortBlock(bc);
ValkeyModule_ReplyWithError(ctx,"Sorry can't create a thread");
}
return VALKEYMODULE_OK;
}
The client will be unblocked, but the reply callback will not be called. periodt. π
π Implementing the command, reply and timeout callback using a single function π
The following functions can be used in order to implement the reply and callback with the same function that implements the primary command function:
int ValkeyModule_IsBlockedReplyRequest(ValkeyModuleCtx *ctx);
int ValkeyModule_IsBlockedTimeoutRequest(ValkeyModuleCtx *ctx);
So I could rewrite the example command without using a separated reply and timeout callback:
int Example_ValkeyCommand(ValkeyModuleCtx *ctx, ValkeyModuleString **argv,
int argc)
{
if (ValkeyModule_IsBlockedReplyRequest(ctx)) {
long *mynumber = ValkeyModule_GetBlockedClientPrivateData(ctx);
return ValkeyModule_ReplyWithLongLong(ctx,mynumber);
} else if (ValkeyModule_IsBlockedTimeoutRequest) {
return ValkeyModule_ReplyWithNull(ctx);
}
ValkeyModuleBlockedClient *bc =
ValkeyModule_BlockClient(ctx,reply_func,timeout_func,NULL,0);
pthread_t tid;
if (pthread_create(&tid,NULL,threadmain,bc) != 0) {
ValkeyModule_AbortBlock(bc);
ValkeyModule_ReplyWithError(ctx,"Sorry can't create a thread");
}
return VALKEYMODULE_OK;
}
and that's where part 1 leaves you bestie, more coming in part 2 π
Functionally it's literally the same thing β no cap, same behavior, same vibes. It's just that some people will prefer the less verbose implementation, the one that concentrates most of the command logic in a single function. One function, all the logic, done. Some of us are minimalists like that π
𧬠Working on copies of data inside a thread π§¬
Okay so here's a genuinely interesting pattern for working with threads that implement the slow part of a command: work with a copy of the data. That way, while some operation is being performed on a key, the user keeps seeing the old version. No freezing, no weird half-states, just the familiar old friend they know π«Ά
But β and this is the slay part β when the thread terminates its work, the representations are swapped, and the new, processed version is the one that gets used. Glow-up complete β¨
An example of this approach is the Neural Redis module, where neural networks are trained in different threads while the user can still execute and inspect their older versions. Training in the background, vibing in the foreground, iconic behavior fr fr π§ π₯
π§΅ Thread safe contexts π§΅
See valkey-modules-api-ref(7) β Thread Safe Contexts in the Modules API reference for how Valkey modules APIs can be called in a safe way from threads. Safe calls only, bestie β go read that one, it's the main character of thread safety π
And that's the page, babe! This was version 9.0.0, dated 2025-10-21, and yes, you just read the whole of VALKEY-MODULES-BLOCKING-OPS(7) π Now go block some ops (responsibly). Byeeeee ππ¦
π¦ where this page comes from
- fedora 44: valkey-doc 9.0.3-2.fc44 Β· CC-BY-SA-4.0
- fedora 44: valkey-doc 9.0.6-1.fc44 Β· CC-BY-SA-4.0
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms