Valkey Scripting
Valkey GLIDE provide an interface for Valkey scripting allowing you to manage and execute custom logic directly on the server. This page will explain how GLIDE handles Valkey scripting and its features.
For more detail on just Valkey scripting, visit the Valkey documentation.
Key Benefits
Section titled “Key Benefits”GLIDE script provide the following advantages:
- Automatic Caching: Scripts are cached server-side using SHA1 hashes, improving performance and reducing overhead
- Cluster Support: Scripts work seamlessly in both standalone and cluster modes
- Management: Built-in support for script routing and lifecycle management
Scripting Basics
Section titled “Scripting Basics”Valkey GLIDE provides a Script class that wraps Valkey’s Lua scripts and handles their execution efficiently.
from glide import Script, GlideClient, GlideClientConfiguration, NodeAddress
# Create a clientconfig = GlideClientConfiguration(addresses=[NodeAddress("localhost", 6379)])client = await GlideClient.create(config)
# Create a simple scriptscript = Script("return 'Hello, Valkey!'")
# Execute the scriptresult = await client.invoke_script(script)print(result) # b'Hello, Valkey!'import glide.api.GlideClient;import glide.api.models.Script;import glide.api.models.configuration.GlideClientConfiguration;import glide.api.models.configuration.NodeAddress;
// Create a clientGlideClientConfiguration config = GlideClientConfiguration.builder() .address(NodeAddress.builder().host("localhost").port(6379).build()) .build();GlideClient client = GlideClient.createClient(config).get();
// Create a simple scriptScript script = new Script("return 'Hello, Valkey!'", false);CompletableFuture<Object> result = client.invokeScript(script);System.out.println(result.get()); // Hello, Valkey!import {GlideClient, Script} from "@valkey/valkey-glide";
// Create a clientconst client = await GlideClient.createClient({ addresses: [{host: "localhost", port: 6379}]});
// Create a simple scriptconst script = new Script("return 'Hello, Valkey!'");
// Execute the scriptconst result = await client.invokeScript(script);console.log(result); // 'Hello, Valkey!'import ( glide "github.com/valkey-io/valkey-glide/go/v2" "github.com/valkey-io/valkey-glide/go/v2/config" "github.com/valkey-io/valkey-glide/go/v2/options")
// Create a clientmyConfig := config.NewClientConfiguration(). WithAddress(&config.NodeAddress{Host: "localhost", Port: 6379})client, err := glide.NewClient(myConfig)
// Create a simple scriptscript := options.NewScript("return 'Hello, Valkey!'")
// Execute the scriptresult, err := client.InvokeScript(context.Background(), *script)fmt.Println(result) // Hello, Valkey!Handling Inputs with KEYS and ARGV
Section titled “Handling Inputs with KEYS and ARGV”Valkey provides KEYS and ARGV to handle input parameters:
- KEYS: Used to pass key names in Valkey (e.g.,
product:123) - ARGV: Used for general input parameters
product_key = "product:shoe:stock"buy_quantity = "3"
result = await client.invoke_script( purchase_script, keys=[product_key], # Maps to KEYS[1] in Lua args=[buy_quantity] # Maps to ARGV[1] in Lua)import glide.api.models.commands.ScriptOptions;
String productKey = "product:shoe:stock";String buyQuantity = "3";
CompletableFuture<Object> result = client.invokeScript( purchaseScript, ScriptOptions.builder() .key(productKey) // Maps to KEYS[1] in Lua .arg(buyQuantity) // Maps to ARGV[1] in Lua .build());const productKey = "product:shoe:stock";const buyQuantity = "3";
const result = await client.invokeScript(purchaseScript, { keys: [productKey], // Maps to KEYS[1] in Lua args: [buyQuantity] // Maps to ARGV[1] in Lua});import "github.com/valkey-io/valkey-glide/go/v2/options"
productKey := "product:shoe:stock"buyQuantity := "3"
scriptOptions := options.NewScriptOptions(). WithKeys([]string{productKey}). // Maps to KEYS[1] in Lua WithArgs([]string{buyQuantity}) // Maps to ARGV[1] in Lua
result, err := client.InvokeScriptWithOptions( context.Background(), *purchaseScript, *scriptOptions,)Why Use KEYS and ARGV
Section titled “Why Use KEYS and ARGV”Scripts are executed physically on a node and thus it is important for the node to contain the keys being accessed.
GLIDE handle this by send the script to the correct node based on the KEYS provided.
If no KEYS are provided, GLIDE will route the script to a random node for execution.
Thus, hardcoding the keys will most likely lead to errors.
For proper execution, make sure to:
- Pass all accessed keys via the
KEYSargument - Only interact with keys listed in
KEYS KEYSmust belong to the same slot (use hashtags likeuser:{100}:nameif needed)
Another benefit is that using ARGV and KEYS allows for efficient caching of scripts.
Since Valkey caches scripts based on their SHA1 hash of the code, hardcoding changing
values directly into the script would create unique scripts each time, preventing effective caching.
# Bad: Creates a new hash for each quantitybad_script = Script(f"return server.call('DECRBY', {product_key}, {buy_quantity})")
# Good: Reuses the same cached scriptgood_script = Script("return server.call('DECRBY', KEYS[1], ARGV[1])")await client.invoke_script(good_script, keys=[product_key], args=[buy_quantity])// Bad: Creates a new hash for each quantityScript badScript = new Script("return redis.call('DECRBY', " + productKey + ", " + buyQuantity + ")", false);
// Good: Reuses the same cached scriptScript goodScript = new Script("return redis.call('DECRBY', KEYS[1], ARGV[1])", false);CompletableFuture<Object> result = client.invokeScript( goodScript, ScriptOptions.builder().key(productKey).arg(buyQuantity).build());// Bad: Creates a new hash for each quantityconst badScript = new Script(`return redis.call('DECRBY', ${productKey}, ${buyQuantity})`);
// Good: Reuses the same cached scriptconst goodScript = new Script("return redis.call('DECRBY', KEYS[1], ARGV[1])");await client.invokeScript(goodScript, {keys: [productKey], args: [buyQuantity]});// Bad: Creates a new hash for each quantitybadScript := options.NewScript(fmt.Sprintf("return redis.call('DECRBY', %s, %s)", productKey, buyQuantity))
// Good: Reuses the same cached scriptgoodScript := options.NewScript("return redis.call('DECRBY', KEYS[1], ARGV[1])")scriptOptions := options.NewScriptOptions(). WithKeys([]string{productKey}). WithArgs([]string{buyQuantity})result, err := client.InvokeScriptWithOptions(context.Background(), *goodScript, *scriptOptions)Script Lifecycle
Section titled “Script Lifecycle”When you create a Script object in GLIDE, it is hashed using SHA1.
GLIDE then automatically caches scripts with each invoke_script() call.
script = Script("return 'Hello'")hash_value = script.get_hash()print(f"Script hash: {hash_value}") # SHA1 hash of the scriptScript script = new Script("return 'Hello'", false);String hashValue = script.getHash();System.out.println("Script hash: " + hashValue); // SHA1 hash of the scriptconst script = new Script("return 'Hello'");const hashValue = script.getHash();console.log(`Script hash: ${hashValue}`); // SHA1 hash of the scriptscript := options.NewScript("return 'Hello'")hashValue := script.GetHash()fmt.Printf("Script hash: %sn", hashValue) // SHA1 hash of the scriptScripts with identical code produce identical hashes, enabling efficient caching and reuse.
script1 = Script("return 'Hello'")script2 = Script("return 'Hello'")assert script1.get_hash() == script2.get_hash() # Same hashScript script1 = new Script("return 'Hello'", false);Script script2 = new Script("return 'Hello'", false);assert script1.getHash().equals(script2.getHash()); // Same hashconst script1 = new Script("return 'Hello'");const script2 = new Script("return 'Hello'");console.assert(script1.getHash() === script2.getHash()); // Same hashscript1 := options.NewScript("return 'Hello'")script2 := options.NewScript("return 'Hello'")// script1.GetHash() == script2.GetHash() // Same hashScripts remain cached on the Valkey server until:
- The server restarts
- Memory pressure triggers eviction
- You explicitly flush the cache with
script_flush()All of which are handled automatically by GLIDE.
To learn more about script lifecycle, see Valkey’s documentation.
Stop Running Script
Section titled “Stop Running Script”A script can be safely killed if it has only performed read-only operations. However, once it executes any write operation, it becomes uninterruptible and must either run to completion or reach a timeout. This prevents data corruption as once a script modifies data it must complete to ensure consistency.
-- Read-only scriptlocal start = server.call('TIME')[1]while server.call('TIME')[1] - start < 10 do server.call('GET', 'some_key') -- Read-onlyendreturn 'Done'-- This script writes data - cannot be killed after the SETserver.call('SET', 'temp', 'value') -- Write operationlocal start = server.call('TIME')[1]while server.call('TIME')[1] - start < 10 do -- Long operation after writeendreturn 'Done'Single-Threaded Execution
Section titled “Single-Threaded Execution”Valkey executes scripts atomically in a single-threaded manner. While a script runs:
- No other commands can execute
- The script has exclusive access to the data
- All operations within the script are guaranteed to complete without interruption
Scripting in Cluster Mode
Section titled “Scripting in Cluster Mode”In cluster mode, Valkey distributes data across multiple nodes using hash slots. Each key is mapped to one of 16,384 slots based on its hash:
slot = CRC16(key) mod 16384GLIDE automatically route scripts to the appropriate nodes.
When executing a script in cluster mode, all KEYS must belong to the same shard.
# This works the same in cluster modescript = Script("return redis.call('SET', KEYS[1], ARGV[1])")result = await cluster_client.invoke_script( script, keys=["user:1000"], # Routed based on key hash args=["John"])// This works the same in cluster modeScript script = new Script("return redis.call('SET', KEYS[1], ARGV[1])", false);CompletableFuture<Object> result = clusterClient.invokeScript( script, ScriptOptions.builder() .key("user:1000") // Routed based on key hash .arg("John") .build());// This works the same in cluster modeconst script = new Script("return redis.call('SET', KEYS[1], ARGV[1])");const result = await clusterClient.invokeScript(script, { keys: ["user:1000"], // Routed based on key hash args: ["John"]});// This works the same in cluster modescript := options.NewScript("return redis.call('SET', KEYS[1], ARGV[1])")scriptOptions := options.NewScriptOptions(). WithKeys([]string{"user:1000"}). // Routed based on key hash WithArgs([]string{"John"})
result, err := clusterClient.InvokeScriptWithOptions( context.Background(), *script, *scriptOptions,)Explicit Routing
Section titled “Explicit Routing”GLIDE provide options for explicit control over script routing in cluster mode.
from glide import SlotKeyRoute, SlotType, AllPrimaries
# Route to the node holding a specific key's slotroute = SlotKeyRoute(SlotType.PRIMARY, "user:1000")
# Route to all primary nodesroute = AllPrimaries()
await cluster_client.invoke_script_route(script, route=route)import static glide.api.models.configuration.RequestRoutingConfiguration.Route.ALL_PRIMARIES;import glide.api.models.configuration.RequestRoutingConfiguration.SlotKeyRoute;import glide.api.models.configuration.RequestRoutingConfiguration.SlotType;
// Route to the node holding a specific key's slotSlotKeyRoute route = new SlotKeyRoute("user:1000", SlotType.PRIMARY);
// Route to all primary nodesCompletableFuture<Object> result = clusterClient.invokeScript(script, ALL_PRIMARIES);import {Routes, SlotType} from "@valkey/valkey-glide";
// Route to the node holding a specific key's slotconst route = {type: "routeByAddress", host: "user:1000", slotType: SlotType.Primary};
// Route to all primary nodesawait clusterClient.invokeScriptWithRoute(script, {route: "allPrimaries"});import "github.com/valkey-io/valkey-glide/go/v2/options"
// Route to the node holding a specific key's slotroute := options.NewSlotKeyRoute("user:1000", options.Primary)
// Route to all primary nodesroute := options.AllPrimaries()
clusterClient.InvokeScriptWithRoute(context.Background(), *script, route)Multi-Slot Scripts
Section titled “Multi-Slot Scripts”In cluster mode, scripts that access multiple keys must ensure all keys belong to the same slot.
Adding a hash tag {} in the key allow you to specify the specific slot.
# Both keys hash to the same slot because of {1000}keys = ["user:{1000}:name", "user:{1000}:email"]// Both keys hash to the same slot because of {1000}String[] keys = {"user:{1000}:name", "user:{1000}:email"};// Both keys hash to the same slot because of {1000}const keys = ["user:{1000}:name", "user:{1000}:email"];// Both keys hash to the same slot because of {1000}keys := []string{"user:{1000}:name", "user:{1000}:email"}To lean more about hash tags, see the documentation.
Batch Operations and Transactions
Section titled “Batch Operations and Transactions”Currently, invoke_script is not supported in batch operations (pipelines/transactions).
To use Lua scripts within an atomic batch (MULTI/EXEC transaction), you must use the EVAL command with custom_command.
# Script with keys and argumentsbatch = Batch(is_atomic=True)batch.custom_command([ "EVAL", "return redis.call('SET', KEYS[1], ARGV[1])", "1", # Number of keys "script-key", # Key "script-value" # Argument])batch.get("script-key")
results = await client.exec(batch, raise_on_error=False)print(f"EVAL result: {results[0]}") # b'OK'print(f"GET result: {results[1]}") # b'script-value'// Script with keys and argumentsTransaction transaction = new Transaction();transaction.customCommand(new String[]{ "EVAL", "return redis.call('SET', KEYS[1], ARGV[1])", "1", // Number of keys "script-key", // Key "script-value" // Argument});transaction.get("script-key");
CompletableFuture<Object[]> results = client.exec(transaction);System.out.println("EVAL result: " + results.get()[0]); // OKSystem.out.println("GET result: " + results.get()[1]); // script-value// Script with keys and argumentsconst transaction = new Transaction();transaction.customCommand([ "EVAL", "return redis.call('SET', KEYS[1], ARGV[1])", "1", // Number of keys "script-key", // Key "script-value" // Argument]);transaction.get("script-key");
const results = await client.exec(transaction);console.log(`EVAL result: ${results[0]}`); // OKconsole.log(`GET result: ${results[1]}`); // script-value// Script with keys and argumentstransaction := options.NewTransaction()transaction.CustomCommand([]string{ "EVAL", "return redis.call('SET', KEYS[1], ARGV[1])", "1", // Number of keys "script-key", // Key "script-value", // Argument})transaction.Get("script-key")
results, err := client.Exec(context.Background(), transaction)fmt.Printf("EVAL result: %vn", results[0]) // OKfmt.Printf("GET result: %vn", results[1]) // script-valueWhat’s Next
Section titled “What’s Next”To learn how execute custom scripts in practice, check out the guide on Valkey scripting.
See our reference page for more examples of custom scripts in GLIDE.
You can learn more about Valkey scripting in the Valkey documentation.