Melian is an in-memory table cache engineered for ultra-low latency reads and predictable performance under load, written in optimized C.
Instead of caching arbitrary keys and pushing invalidation logic into your application, Melian materializes full or partial database tables into coherent in-memory snapshots and refreshes them on a schedule. Snapshots are swapped atomically: readers never observe half-updated data.
These were run against a MacBook Pro M3 (36GB mem).
- Melian plateaus at ~580k RPS with p99 ≤ 256 µs
- Redis reaches ~300k RPS with p99 up to 1024 µs
- Melian has ~2× higher peak throughput and 2–4× lower p99 latency at all concurrency levels
- Stable throughput plateau from 16–256 connections
Concurrency | Melian RPS | Redis RPS | Melian p99 | Redis p99
------------+------------+-----------+------------+-----------
1 | 157k | 97k | 8 µs | 16 µs
16 | 564k | 265k | 32 µs | 64 µs
32 | 586k | 285k | 32 µs | 128 µs
64 | 578k | 297k | 128 µs | 256 µs
256 | 578k | 309k | 256 µs | 1024 µs
Analyzed:
Concurrency | Throughput (Melian > Redis) | Tail latency (p99)
------------+------------------------------+--------------------
1 | 1.6× | 2× lower
16 | 2.1× | 2× lower
32 | 2.1× | 4× lower
64 | 1.9× | 2× lower
256 | 1.9× | 4× lower
- Blazing fast lookups: Sub-millisecond query latency, zero-copy networking, low CPU use, stable memory.
- Concurrency: Lock-free reads, atomic data swap on reload.
- Periodic refresh: automatic data reload via a background thread, not event-driven.
- Data model: Full- or partial-table snapshots, but not individual keys.
- Consistency: Always serves complete, coherent snapshots - no half-updated data.
- Dual-key indexing: look up entries by numeric or string key.
- Clients in Node.js, Python, C, Perl, PHP, and Raku.
- Runtime performance statistics: query table size, min/max ID, and memory usage.
- Binary row payloads: length-prefixed field name/type/value encoding for fast decode and byte-accurate values.
Most applications just need specific tables to always be in memory for fast reads.
Traditional caches (Redis, Memcached) require your app to manage keys manually (and often individually) and synchronize them with the database. That adds complexity and risks serving stale or inconsistent data. Being flexible to these changes also forces them to perform slower than they could.
Melian is a good fit when:
- You have one or more tables in PostgreSQL/MySQL/MariaDB/SQLite
- Those tables change periodically (minute/hour/day/...)
- You want them always in memory for very fast reads
- You care about snapshot consistency (no partial refresh states)
Typical examples:
- Reference tables (countries, currencies, plans, services, permissions).
- Host or customer routing maps.
- User or organizational metadata used across many services.
- Materialized data sets refreshed periodically.
- Read-mostly microservices.
Melian is not a good fit when:
- You need instant propagation of updates (write-through caching)
- You need arbitrary key/value caching not expressible as a table query
- You want clustering/replication built in
- You need to mutate cache state frequently
- Gonzalo "Gonzo" Diethelm (@gonzus)
- Sawyer X (@xsawyerx)
# add flags when deps are not in the default paths
$ ./configure --with-mysql=/path/to/mysql \ # enable MySQL/MariaDB support
--with-postgresql=/path/to/pgsql \ # enable PostgreSQL support
--with-sqlite3=/path/to/sqlite \ # enable SQLite support
--with-libevent=/path/to/libevent \
--with-jansson=/path/to/jansson
$ make
$ make installThe configure step fails explicitly if the required headers/libraries cannot be located. At least one database backend (PostgreSQL, MariaDB/MySQL, or SQLite) must be available, configure stops early otherwise. Pass whichever --with-* flags match the drivers you intend to compile in.
- Start the server
# Run listening on a UNIX socket (default)
$ ./melian-server
# Run listening on a TCP socket only
$ MELIAN_SOCKET_PATH= MELIAN_SOCKET_PORT=42123 ./melian-server
# Run listening on both UNIX and TCP simultaneously
$ MELIAN_SOCKET_PORT=42123 ./melian-server
# Display server options
$ ./melian-server --help
# Display server version
$ ./melian-server --versionSet the database driver explicitly and adjust shared settings via config file or environment variables.
{
"database": {
"driver": "sqlite",
"name": "melian",
"host": "localhost",
"port": 42123,
"username": "melian",
"password": "melian",
"sqlite": {
"filename": "/var/lib/melian.db"
}
},
"socket": {
"path": "/tmp/melian.sock",
"host": "127.0.0.1",
"port": 42123
},
"table": {
"period": 60,
"selects": {
"table1": "SELECT id, email FROM table1",
"table2": "SELECT id, hostname FROM table2 WHERE active = 1"
}
},
"tables": [
{
"name": "table1",
"id": 0,
"period": 60,
"indexes": [
{
"id": 0,
"column": "id",
"type": "int"
}
]
},
{
"name": "table2",
"id": 1,
"period": 60,
"indexes": [
{
"id": 0,
"column": "id",
"type": "int"
},
{
"id": 1,
"column": "hostname",
"type": "string"
}
]
}
]
}
You can store the entire configuration in a JSON file and tell the server to load it at startup:
$ ./melian-server -c /path/to/melian-config.json
# or
$ ./melian-server --configfile /path/to/melian-config.jsonConfiguration sources are consulted in this order:
- Command-line
-c/--configfile. - Environment variable
MELIAN_CONFIG_FILE. - Default
/etc/melian.json.
Any values from the configuration file are still overridable by environment variables so you can keep secrets out of the file (e.g., inject MELIAN_DB_PASSWORD at runtime while other settings live in JSON).
These will override any values in the config file.
MELIAN_DB_DRIVER(config:database.driver):mysql,postgresql, orsqlite(required)MELIAN_DB_HOST(config:database.host): database host (default127.0.0.1)MELIAN_DB_PORT(config:database.port): database port (default3306)MELIAN_DB_NAME(config:database.name): database/schema name (defaultmelian)MELIAN_DB_USER(config:database.username): username (defaultmelian)MELIAN_DB_PASSWORD(config:database.password): password (defaultmeliansecret)MELIAN_SQLITE_FILENAME(config:database.sqlite.filename): SQLite database filename (default/etc/melian.db)MELIAN_SOCKET_HOST(config:socket.host): TCP bind address (default127.0.0.1)MELIAN_SOCKET_PORT(config:socket.port): TCP port --0to disable (default0)MELIAN_SOCKET_PATH(config:socket.path): UNIX socket path -- empty to disable (default/tmp/melian.sock)
Both UNIX and TCP listeners can be active simultaneously. By default only the UNIX socket is enabled. Set MELIAN_SOCKET_PORT to a non-zero value to also enable TCP.
MELIAN_SERVER_TOKENS(config:server.tokens): whether to advertise the server version in status JSON (defaulttrue)MELIAN_TABLE_PERIOD(config:table.period):60seconds (reload interval)MELIAN_TABLE_SELECTS(config:table.selects): semicolon-separated overrides (table=SELECT ...;table2=SELECT ...) to customize per-table SELECT statementsMELIAN_TABLE_TABLES(config:tables):table1,table2
When using MELIAN_TABLE_SELECTS, ensure each entry follows table_name=SELECT ... and separate multiple entries with ;. The SQL is used verbatim, so double-check statements for the intended tables.
In JSON, use table.selects with a mapping of table names to their statements:
"table": {
"selects": {
"table1": "SELECT ...",
"table2": "SELECT ..."
}
}The server version is compiled in protocol.h as MELIAN_SERVER_VERSION. Use --version to print it. If you want to hide the version from the status JSON, set MELIAN_SERVER_TOKENS=false or server.tokens: false in the config file.
- Use the test client
Ths describes the test client we have in C.
# Connect to a UNIX socket
$ ./melian-client -u /tmp/melian.sock
# Connect to a TCP socket (start server with TCP enabled)
$ MELIAN_SOCKET_PORT=42123 ./melian-server
$ ./melian-client -p 42123
# Display client options
$ ./melian-client -?
$ ./melian-client -hfetch: Fetch a single row (see Ad-hoc querying)schema: Show the server schema as JSONstats: Show server statistics as JSON
When no subcommand is given, the client runs in benchmark mode:
-U: Benchmark table1 by ID-C: Benchmark table2 by ID-H: Benchmark table2 by hostname-s: Print server statistics-q: Quit the server-v: Verbose logging
Melian uses a binary protocol, so curl won't work. The C client supports subcommands for quick, ad-hoc queries against a running server.
All examples below assume a UNIX socket at /tmp/melian.sock. For TCP, replace -u /tmp/melian.sock with -p PORT (and optionally -h HOST).
Describe schema (discover tables and indexes):
./melian-client -u /tmp/melian.sock schemaFetch a row by integer key (table table1, index id, key 42):
./melian-client -u /tmp/melian.sock fetch --table table1 --index id --key 42Fetch a row by string key (table table2, index hostname, key host-00002):
./melian-client -u /tmp/melian.sock fetch --table table2 --index hostname --key host-00002Mix names and IDs freely (table by ID, index by name):
./melian-client -u /tmp/melian.sock fetch --table-id 1 --index hostname --key host-00002Server statistics:
./melian-client -u /tmp/melian.sock statsThe key type (integer or string) is detected automatically from the schema - no need to specify it. Output is JSON, suitable for piping through jq.
The provided Dockerfile builds a self-contained image (SQLite + bundled clients). Build it locally:
docker build -t melian:latest .This keeps the UNIX domain socket enabled and bind-mounts it to the host so native clients can connect:
mkdir -p $(pwd)/socket
docker run --rm \
-p 42123:42123 \
-v $(pwd)/socket:/run/melian \
melian:latestThe server listens on /run/melian/melian.sock inside the container. On the host you’ll find the mirrored socket at ./socket/melian.sock.
Enable both listeners by setting a port while keeping the default socket path:
docker run --rm \
-v $(pwd)/socket:/run/melian \
-e MELIAN_SOCKET_HOST=0.0.0.0 \
-e MELIAN_SOCKET_PORT=42123 \
-p 42123:42123 \
melian:latestTo accept only TCP connections, unset MELIAN_SOCKET_PATH and map the port:
docker run --rm \
-e MELIAN_SOCKET_PATH= \
-e MELIAN_SOCKET_HOST=0.0.0.0 \
-e MELIAN_SOCKET_PORT=42123 \
-p 42123:42123 \
melian:latestClients can then connect to tcp://localhost:42123.
The default Unix socket is created with mode 0660 (owner + group read/write). Only processes running as the same user or group can connect. No additional configuration is needed.
When exposing Melian over TCP, bind to a specific interface rather than 0.0.0.0 and use firewall rules to restrict access to trusted hosts:
# Bind Melian to an internal interface
MELIAN_SOCKET_HOST=10.0.1.5 MELIAN_SOCKET_PORT=42123 ./melian-server# Allow only your application subnet, drop everything else
iptables -A INPUT -p tcp --dport 42123 -s 10.0.1.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 42123 -j DROPThis is handled at the kernel level with zero performance overhead.
Melian does not include built-in TLS. Its performance comes from zero-copy direct I/O on raw sockets. In-process TLS would add a 30-50% throughput penalty (the same cost Redis 6+ pays for native TLS).
For encrypted connections, run a TLS termination proxy in front of Melian. stunnel, HAProxy, and nginx all work. Example stunnel configuration:
[melian]
accept = 0.0.0.0:42123
connect = /tmp/melian.sock
cert = /etc/melian/server.pem
key = /etc/melian/server.keyClients connect to tls://host:42123, stunnel decrypts and forwards to Melian's Unix socket. For containerized deployments, run the TLS proxy as a sidecar container alongside Melian.