A pure-Odin Redis client with no external dependencies.
redin is a lightweight Redis client for the Odin programming language, implementing the RESP2 (REdis Serialization Protocol v2) wire protocol from scratch over a plain TCP socket. It covers all major Redis data structures and includes support for pipelining.
The initial codebase was entirely generated by Claude (Anthropic) and subsequently reviewed, tested and refined by the author.
- Zero external dependencies — only Odin's
corelibrary - Full RESP2 parser (Simple String, Error, Integer, Bulk String, Array, Null)
- High-level API for Strings, Lists, Hashes, Sets, Sorted Sets
- Pipelining support (multiple commands in a single round-trip)
- Raw
execescape hatch for any command not covered by the API - Optional password auth and database selection
redin/
├── redis.odin # client library
└── example/
└── main.odin # usage examples
Make sure Redis is running locally, then:
# Build the example
odin build example/ -out:redis_example
# Run it (requires a Redis instance on 127.0.0.1:6379)
./redis_examplec, err := redis.connect("127.0.0.1", 6379)
if err != "" { /* handle */ }
defer redis.close(&c)
redis.auth(&c, "password") // optional authentication
redis.select_db(&c, 1) // select database (default: 0)redis.set(&c, "key", "value")
redis.set_ex(&c, "token", "abc", 3600) // expires in 1h
redis.set_nx(&c, "lock", "1") // set only if not exists
val, found, err := redis.get(&c, "key")
redis.incr(&c, "counter")
redis.incrby(&c, "counter", 5)
redis.decr(&c, "counter")
redis.decrby(&c, "counter", 3)
redis.mset(&c, "k1", "v1", "k2", "v2")
values, _ := redis.mget(&c, "k1", "k2", "missing")
redis.append_str(&c, "log", " entry")
redis.strlen(&c, "key")redis.rpush(&c, "list", "a", "b", "c")
redis.lpush(&c, "list", "z")
items, _ := redis.lrange(&c, "list", 0, -1)
redis.llen(&c, "list")
redis.lpop(&c, "list")
redis.rpop(&c, "list")
redis.lindex(&c, "list", 0)
redis.lset(&c, "list", 0, "new")
redis.lrem(&c, "list", 1, "a")
redis.ltrim(&c, "list", 0, 9)redis.hset(&c, "user:1", "name", "Alice")
redis.hmset(&c, "user:1", "name", "Alice", "age", "30", "city", "Rome")
val, found, _ := redis.hget(&c, "user:1", "name")
m, _ := redis.hgetall(&c, "user:1") // → map[string]string
redis.hdel(&c, "user:1", "city")
redis.hexists(&c, "user:1", "name")
redis.hincrby(&c, "user:1", "age", 1)
redis.hkeys(&c, "user:1")
redis.hvals(&c, "user:1")
redis.hlen(&c, "user:1")redis.sadd(&c, "tags", "odin", "redis", "programming")
redis.srem(&c, "tags", "programming")
redis.smembers(&c, "tags")
redis.sismember(&c, "tags", "odin") // → bool
redis.scard(&c, "tags")
redis.sunion(&c, "tags", "more_tags")
redis.sinter(&c, "tags", "other_tags")
redis.sdiff(&c, "tags", "other_tags")redis.zadd(&c, "leaderboard", 100.0, "alice")
redis.zadd(&c, "leaderboard", 85.5, "bob")
redis.zrange(&c, "leaderboard", 0, -1) // members only
redis.zrange(&c, "leaderboard", 0, -1, true) // with scores
rank, found, _ := redis.zrank(&c, "leaderboard", "alice")
score, found, _ := redis.zscore(&c, "leaderboard", "alice")
redis.zrem(&c, "leaderboard", "bob")
redis.zcard(&c, "leaderboard")redis.del(&c, "k1", "k2", "k3")
redis.exists(&c, "k1")
redis.expire(&c, "k1", 60) // seconds
redis.pexpire(&c, "k1", 5000) // milliseconds
redis.ttl(&c, "k1")
redis.pttl(&c, "k1")
redis.persist(&c, "k1") // remove expiry
redis.keys(&c, "prefix:*")
redis.rename(&c, "old", "new")
redis.key_type(&c, "k1") // "string" | "list" | "hash" | "set" | "zset"redis.ping(&c)
redis.dbsize(&c)
redis.flushdb(&c)
redis.flushall(&c)Send multiple commands in a single round-trip for maximum throughput:
pipe := redis.pipeline_begin(&c)
redis.pipeline_queue(&pipe, "SET", "k1", "v1")
redis.pipeline_queue(&pipe, "SET", "k2", "v2")
redis.pipeline_queue(&pipe, "GET", "k1")
redis.pipeline_queue(&pipe, "GET", "k2")
results, err := redis.pipeline_exec(&pipe)
for r in results {
redis.value_print(r)
}For any command not covered by the high-level API:
v, err := redis.exec(&c, "COMMAND", "COUNT")
v, err = redis.exec(&c, "CONFIG", "GET", "maxmemory")
redis.value_print(v)All responses are parsed into a Value union:
Value_Kind :: enum {
Simple_String,
Error,
Integer,
Bulk_String,
Array,
Null,
}
Value :: struct {
kind: Value_Kind,
str: string, // Simple_String | Bulk_String | Error
integer: i64, // Integer
array: []Value, // Array
}Use redis.value_destroy(&v) to recursively free a Value and all its children.
- Strings returned by high-level procedures are allocated with
strings.clone— free them when done, or use an arena allocator for the whole request. - Use
redis.value_destroy(&v)to recursively free rawValueresponses. Client.bufis a dynamic buffer allocated onconnectand freed byclose.
MIT — see LICENSE for details.
Initial code fully generated by Claude (Anthropic) · reviewed and refined by the author.