A tiny in-memory key-value cache shared between processes via a Unix socket.
Find a file
2026-04-29 15:55:43 +00:00
cmd/cubby fix missing definition for group flag 2026-04-29 15:55:43 +00:00
contrib/systemd add sample systemd files 2026-04-29 15:47:00 +00:00
.gitignore Initial commit 2026-04-29 03:10:17 +00:00
client.go add a client to use cubby in go projects 2026-04-29 12:36:08 +00:00
client_test.go add a client to use cubby in go projects 2026-04-29 12:36:08 +00:00
go.mod add simple memory cache server 2026-04-29 12:31:00 +00:00
LICENSE Initial commit 2026-04-29 03:10:17 +00:00
README.md add README 2026-04-29 12:37:14 +00:00
testhelpers_test.go add a client to use cubby in go projects 2026-04-29 12:36:08 +00:00

cubby

A tiny in-memory key-value cache shared between processes via a Unix socket.

Stash a value, fetch it from another process. That's it.

Run the server

go run ./cmd/cubby                       # default: /tmp/cubby.sock, owner-only (0600)
go run ./cmd/cubby -socket /tmp/c.sock   # custom path
go run ./cmd/cubby -group cache          # share with members of "cache" group (0660)
go run ./cmd/cubby -group 1001           # numeric GID also works

Use it from Go

import "cubby"

c, err := cubby.Dial("/tmp/cubby.sock")
if err != nil { ... }
defer c.Close()

c.Set("hello", "world", 0)              // no TTL
c.Set("session", "abc", 5*time.Second)  // expires in 5s

v, ok, err := c.Get("hello")            // ok=false means missing (not an error)

existed, _ := c.Del("hello")
keys, _ := c.Keys()

A Client is a single connection and isn't safe for concurrent use. For goroutines, use a Pool:

pool := cubby.NewPool("/tmp/cubby.sock", 8)  // up to 8 idle conns
defer pool.Close()

err := pool.Do(func(c *cubby.Client) error {
    return c.Set("k", "v", 0)
})

See examples/basic for a runnable example.

Sharing with a group

By default the socket is chmod 0600 and chowned to the user running cubby — only that user can connect. With -group <name>, the socket is created 0660 and chowned to that group, so any user in the group can connect. Everyone else is still locked out.

To set this up:

sudo groupadd cache               # create the group (one-time)
sudo usermod -aG cache alice      # add each user who should connect
sudo usermod -aG cache bob
# users need to log out and back in for new group membership to take effect
go run server.go -group cache

Talk to it

The protocol is line-based, so you can poke at it with nc -U:

$ nc -U /tmp/cubby.sock
PING
+PONG
SET hello world
+OK
GET hello
$"world"
SET token abc 5
+OK
DEL hello
#1
KEYS
#1
$"token"
QUIT
+BYE

Protocol

Commands (one per line, whitespace-separated, values can be Go-quoted to include spaces or newlines):

Command Meaning
SET <key> <value> [ttl_seconds] Stash a value, optional TTL
GET <key> Fetch a value
DEL <key> Remove a key
KEYS List all live keys
PING Health check
QUIT Close connection

Replies (single sigil, terminated by \n):

Sigil Meaning
+text OK / simple string
-text Error
$"value" Go-quoted string value
_ Nil (key not found)
#n Count (integer)

KEYS replies with #N followed by N $"..." lines.

Design notes

  • Storage is a single map[string]entry behind a sync.RWMutex. Plenty for a process-local cache. Shard by key hash if you ever need more.
  • Expiry is checked lazily on read and swept once per second.
  • Quoting uses Go's strconv.Quote/Unquote, so values can contain arbitrary bytes including spaces, tabs, and newlines.
  • One goroutine per connection — idiomatic Go, no event loop needed.
  • Socket permissions are set via umask before Listen, so there's no race where the socket exists with looser bits.
  • Clean shutdown on SIGINT/SIGTERM removes the socket file.

What it doesn't do

No persistence, pub/sub, transactions, or data types beyond strings. It's a shared in-memory cache. If you need any of that, reach for something bigger.