My Use Case Link to heading
The main API runs on AWS Lambda, and I needed a super fast caching layer for basic data. I literally needed a bare-bones key/value store, no need for persistence, replication, modules, Lua scripting, eviction policies, fine-grained ACLs, or the universe of configuration flags that comes with general-purpose data stores.
The combination of small values, tight latency, and owned endpoints made existing options feel like a mismatch. Systems like Redis and Valkey are excellent; they’re also designed to solve larger, broader problems than mine. Even when you turn features off, you still inherit the operational surface.
What I actually needed Link to heading
My workload is very read-heavy. Think on the order of 10,000 reads for every write. My data is also small and boring, a handful of bytes to a few hundred characters. The consumer and the producer are both mine. That single ownership is the key enabler; it lets me dictate the wire format and API contract without worrying about third-party clients.
I don’t need data to survive a process restart, and I don’t need cross-region fail-over. What I do need is low and predictable latency.
The Needs List Link to heading
-
Read-heavy: ~10,000 reads for every write
-
Small data: a handful of bytes to a few hundred characters
-
Single ownership: both consumer and producer are mine, so I dictate wire format and API contract
-
No persistence: data doesn’t need to survive restarts
-
No cross-region failover
-
Requirement: low, predictable latency
Why UDP and fixed frames? Link to heading
The first architectural decision was the transport layer. I choose UDP for one reason: one datagram equals one operation. There’s no connection handshake or connection state to manage, and for tiny messages the kernel does less work. The usual caveats about UDP, packet loss and reordering, are real, but in our environment the network is stable, the messages are small, and the consumer is prepared to timeout and retry. I place a strict requirement on callers; you must provide a context with a deadline. If the request doesn’t complete within those bounds, the call fails. That simple rule, no deadline no request, eliminates an entire class of mystery lags.
On top of UDP, I defined a compact, fixed-size frame. I encrypt the entire frame using AES-256-GCM with a unique per-message nonce.
The Frame Link to heading
-
1 byte per command limits us to 256 commands ever
-
4 bytes for flags limits us to a maximum of 32 flags
-
128 bytes for the key
-
863 bytes for the value (chosen so the full frame fits within 1024 bytes)
This gives us a total of 996 bytes
The Encryption Link to heading
-
12 bytes for the nonce
-
16 bytes for the tag
This gives us a total encrypted packet size of 1024 bytes, comfortably below the typical 1500-byte Ethernet MTU. That ensures a single packet per operation without fragmentation.
One note is that putting the value at the end, means in the future if I decide I need an additional field I can carve it out of the value.
Because I own both ends, I can keep security simple and non-optional. Every request and response is encrypted with AES-256-GCM using a 32-byte key.
The developer experience Link to heading
A system this small should be easy to use without memorizing a manual. The client library is a slim wrapper over the wire format. Callers construct a context with a timeout and call Set, Get, Delete, or Exists. For Set, there are two flags that cover the only interesting write behaviors I need: Overwrite (allow an existing key to be replaced) and Old (return the previous value if I want it). Encryption, frame building, deadlines applied to read and write, is handled internally. For local testing, there’s a CLI that mirrors the library and makes it trivial to poke the server from a shell.
More importantly, the library has sharp edges where they help. If you forget to set a deadline, the call fails immediately. If a packet can’t be decrypted or the frame isn’t exactly the expected size, it’s rejected. You don’t get a best-effort result; you get a clear error you can act on. That’s the sort of “pragmatic strictness” that keeps small systems healthy.
What this wins us Link to heading
The obvious win is latency. In practice, fixed frames over UDP with a short data path give you consistently low numbers on localhost and inside a VPC. But the subtler wins matter just as much. Operationally, there’s less to configure, fewer ways to misconfigure, and fewer background behaviors to learn. Auditing is easy because the code is small; you can trace a request from socket read to map write in a minute. When something fails, it fails in a small number of explicit places. And because I only implemented the features I actually use, there’s nothing mysterious happening behind the scenes.
There’s also a win in predictability. Fixed size, explicit deadlines, and a tiny command set mean the system behaves the same way everywhere. You’re not juggling persistence modes, eviction policies, or subtle differences in server versions. The store either answers inside your deadline or it doesn’t, and both outcomes are observable and expected.
Where the tradeoffs land Link to heading
I was deliberate about what I didn’t build. There is no durability; if the process restarts, the data is gone. There’s no replication or clustering; one instance serves one purpose. UDP doesn’t guarantee delivery or ordering; I accept that and designed my callers accordingly. The command set is intentionally narrow; you won’t find data structures, transactions, or scripts. If any of those are requirements, this tool isn’t the right fit, and that’s fine. Mature systems exist to solve those problems, and you should use them.
Security also follows the environment, not the public internet. I rely on a shared secret and a trusted network boundary. If you need per-client identities, rotation policies, or complex authorization, this design would need to evolve. Similarly, if you need observability at scale, multi-tenant metrics, distributed tracing, you’d add those hooks over time. Even in its minimal form, simple counters (ops/sec, errors) are easy to bolt on without changing the wire format. None of that changes the core wire format, but it adds complexity I don’t need today.
Why build instead of buy Link to heading
The honest answer is that the delta between “what I need” and “what an off-the-shelf system provides” was larger than it looked. Even when you disable features, you still inherit their operational surface area: configuration to keep them off, documentation you have to read to be sure, operational knowledge you have to maintain just in case. The domain didn’t justify that overhead. By writing the minimal thing, encrypted UDP, fixed frames, a guarded map, four commands, I ended up with software that is easier to run, easier to test, and easier to understand than any trimmed-down general solution I tried.
I didn’t build this to be clever. I built it because it was the most boring way to solve my specific problem. Boring is good. Boring is reliable.
When you should (and shouldn’t) do the same Link to heading
If your use case looks like mine, small values, read heavy, owned endpoints, trusted network, tight latency budgets, then a tiny encrypted UDP key–value store can be the pragmatic choice. You’ll get simplicity, predictability, and a short mean-time-to-understanding when something breaks. If you need durability, multi-tenant security, complex data structures, or internet-facing hardening, don’t roll your own. Use the battle-tested tools that exist for exactly those needs. Boring is still good, but only if it’s boring in the right problem space.
Closing Link to heading
Building our own store wasn’t about outperforming Redis on benchmarks or reinventing the wheel. It was about choosing a smaller wheel that fits the cart. By sticking to a fixed frame, enforcing deadlines, and making encryption mandatory, I got a tool that does one job quickly and predictably, with almost nothing to babysit. That’s the kind of system you can keep in your head and that’s exactly what I wanted. And if my needs grow, migrating to Redis or Valkey later is straightforward because the client API is deliberately minimal.