
Security News
Follow-up and Clarification on Recent Malicious Ruby Gems Campaign
A clarification on our recent research investigating 60 malicious Ruby gems.
Mudis is a fast, thread-safe, in-memory, sharded LRU (Least Recently Used) cache for Ruby applications. Inspired by Redis, it provides value serialization, optional compression, per-key expiry, and metric tracking in a lightweight, dependency-free package that lives inside your Ruby process.
It’s ideal for scenarios where performance and process-local caching are critical, and where a full Redis setup is overkill or otherwise not possible/desirable.
Alternatively, Mudis can be upscaled with higher sharding and resources in a dedicated Rails app to provide a Mudis server.
There are plenty out there, in various states of maintenance and in many shapes and sizes. So why on earth do we need another? I needed a drop-in replacement for Kredis, and the reason I was interested in using Kredis was for the simplified API and keyed management it gave me in extension to Redis. But what I didn't really need was Redis. I needed an observable, fast, simple, easy to use, flexible and highly configurable, thread-safe and high performant caching system which didn't require too many dependencies or standing up additional services. So, Mudis was born. In its most rudimentary state it was extremely useful in my project, which was an API gateway connecting into mutliple micro-services and a wide selection of APIs. The majority of the data was cold and produced by repeat expensive queries across several domains. Mudis allowed for me to minimize the footprint of the gateway, and improve end user experience, and increase performance. So, yeah, there's a lot of these gems out there, but none which really met all my needs. I decided to provide Mudis for anyone else. If you use it, I'd be interested to know how and whether you got any benefit.
Feature | Mudis | MemoryStore (Rails.cache ) | FastCache | Zache | EasyCache | MiniCache |
---|---|---|---|---|---|---|
LRU eviction strategy | ✅ Per-bucket | ✅ Global | ✅ Global | ❌ | ❌ | ✅ Simplistic |
TTL expiry support | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Background expiry cleanup thread | ✅ | ❌ (only on access) | ❌ | ✅ | ❌ | ❌ |
Thread safety | ✅ Bucketed | ⚠️ Global lock | ✅ Fine-grained | ✅ | ⚠️ | ⚠️ |
Sharding (buckets) | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
Custom serializers | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
Compression (Zlib) | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
Hard memory cap | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
Max value size enforcement | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
Metrics (hits, misses, evictions) | ✅ | ⚠️ Partial | ❌ | ❌ | ❌ | ❌ |
Fetch/update pattern | ✅ Full | ✅ Standard | ⚠️ Partial | ✅ Basic | ✅ Basic | ✅ Basic |
Namespacing | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
Replace (if exists) | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
Clear/delete method | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Key inspection with metadata | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
Concurrency model | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
Maintenance level | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | ⚠️ |
Suitable for APIs or microservices | ✅ | ⚠️ Limited | ✅ | ⚠️ Small apps | ⚠️ Small apps | ❌ |
Add this line to your Gemfile:
gem 'mudis'
Or install it manually:
gem install mudis
In your Rails app, create an initializer:
# config/initializers/mudis.rb
Mudis.configure do |c|
c.serializer = JSON # or Marshal | Oj
c.compress = true # Compress values using Zlib
c.max_value_bytes = 2_000_000 # Reject values > 2MB
c.hard_memory_limit = true # enforce hard memory limits
c.max_bytes = 1_073_741_824 # set maximum cache size
end
Mudis.start_expiry_thread(interval: 60) # Cleanup every 60s
at_exit do
Mudis.stop_expiry_thread
end
Or with direct setters:
Mudis.serializer = JSON # or Marshal | Oj
Mudis.compress = true # Compress values using Zlib
Mudis.max_value_bytes = 2_000_000 # Reject values > 2MB
Mudis.hard_memory_limit = true # enforce hard memory limits
Mudis.max_bytes = 1_073_741_824 # set maximum cache size
Mudis.start_expiry_thread(interval: 60) # Cleanup every 60s
## set at exit hook
require 'mudis'
# Write a value with optional TTL
Mudis.write('user:123', { name: 'Alice' }, expires_in: 600)
# Read it back
Mudis.read('user:123') # => { "name" => "Alice" }
# Check if it exists
Mudis.exists?('user:123') # => true
# Atomically update
Mudis.update('user:123') { |data| data.merge(age: 30) }
# Delete a key
Mudis.delete('user:123')
Mudis provides utility methods to help with test environments, console debugging, and dev tool resets.
Mudis.reset!
Clears the internal cache state. Including all keys, memory tracking, and metrics. Also stops the expiry thread.
Mudis.write("foo", "bar")
Mudis.reset!
Mudis.read("foo") # => nil
Mudis.reset_metrics!
Clears only the metric counters and preserves all cached values.
Mudis.write("key", "value")
Mudis.read("key") # => "value"
Mudis.metrics # => { hits: 1, misses: 0, ... }
Mudis.reset_metrics!
Mudis.metrics # => { hits: 0, misses: 0, ... }
Mudis.read("key") # => "value" (still cached)
Mudis.least_touched
Returns the top n
(or all) keys that have been read the fewest number of times, across all buckets. This is useful for identifying low-value cache entries that may be safe to remove or exclude from caching altogether.
Each result includes the full key and its access count.
Mudis.least_touched
# => [["foo", 0], ["user:42", 1], ["product:123", 2], ...]
Mudis.least_touched(5)
# => returns top 5 least accessed keys
Mudis.keys(namespace:)
Returns all keys for a given namespace.
Mudis.write("u1", "alpha", namespace: "users")
Mudis.write("u2", "beta", namespace: "users")
Mudis.keys(namespace: "users")
# => ["u1", "u2"]
Mudis.clear_namespace(namespace:)
Deletes all keys within a namespace.
Mudis.clear_namespace("users")
Mudis.read("u1", namespace: "users") # => nil
For simplified or transient use in a controller, you can wrap your cache logic in a reusable thin class:
class MudisService
attr_reader :cache_key, :namespace
# Initialize the service with a cache key and optional namespace
#
# @param cache_key [String] the base key to use
# @param namespace [String, nil] optional logical namespace
def initialize(cache_key, namespace: nil)
@cache_key = cache_key
@namespace = namespace
end
# Write a value to the cache
#
# @param data [Object] the value to cache
# @param expires_in [Integer, nil] optional TTL in seconds
def write(data, expires_in: nil)
Mudis.write(cache_key, data, expires_in: expires_in, namespace: namespace)
end
# Read the cached value or return default
#
# @param default [Object] fallback value if key is not present
def read(default: nil)
Mudis.read(cache_key, namespace: namespace) || default
end
# Update the cached value using a block
#
# @yieldparam current [Object] the current value
# @yieldreturn [Object] the updated value
def update
Mudis.update(cache_key, namespace: namespace) { |current| yield(current) }
end
# Delete the key from cache
def delete
Mudis.delete(cache_key, namespace: namespace)
end
# Return true if the key exists in cache
def exists?
Mudis.exists?(cache_key, namespace: namespace)
end
# Fetch from cache or compute and store it
#
# @param expires_in [Integer, nil] optional TTL
# @param force [Boolean] force recomputation
# @yield return value if key is missing
def fetch(expires_in: nil, force: false)
Mudis.fetch(cache_key, expires_in: expires_in, force: force, namespace: namespace) do
yield
end
end
# Inspect metadata for the current key
#
# @return [Hash, nil] metadata including :expires_at, :created_at, :size_bytes, etc.
def inspect_meta
Mudis.inspect(cache_key, namespace: namespace)
end
end
Use it like:
cache = MudisService.new("user:42:profile", namespace: "users")
cache.write({ name: "Alice" }, expires_in: 300)
cache.read # => { "name" => "Alice" }
cache.exists? # => true
cache.update { |data| data.merge(age: 30) }
cache.fetch(expires_in: 60) { expensive_query }
cache.inspect_meta # => { key: "users:user:42:profile", ... }
Track cache effectiveness and performance:
Mudis.metrics
# => {
# hits: 15,
# misses: 5,
# evictions: 3,
# rejected: 0,
# total_memory: 45678,
# least_touched: [
# ["user:1", 0],
# ["post:5", 1],
# ...
# ],
# buckets: [
# { index: 0, keys: 12, memory_bytes: 12345, lru_size: 12 },
# ...
# ]
# }
Optionally, return these metrics from a controller for remote analysis and monitoring if using Rails.
class MudisController < ApplicationController
def metrics
render json: { mudis: Mudis.metrics }
end
end
Setting | Description | Default |
---|---|---|
Mudis.serializer | JSON, Marshal, or Oj | JSON |
Mudis.compress | Enable Zlib compression | false |
Mudis.max_value_bytes | Max allowed size in bytes for a value | nil (no limit) |
Mudis.buckets | Number of cache shards | 32 |
Mudis.start_expiry_thread | Background TTL cleanup loop (every N sec) | Disabled by default |
Mudis.hard_memory_limit | Enforce hard memory limits on key size and reject if exceeded | false |
Mudis.max_bytes | Maximum allowed cache size | 1GB |
Mudis.max_ttl | Set the maximum permitted TTL | nil (no limit) |
Mudis.default_ttl | Set the default TTL for fallback when none is provided | nil |
Buckets can also be set using a MUDIS_BUCKETS
environment variable.
When setting serializer
, be mindful of the below
Serializer | Recommended for |
---|---|
Marshal | Ruby-only apps, speed-sensitive logic |
JSON | Cross-language interoperability |
Oj | API-heavy apps using JSON at scale |
100000 iterations
Serializer | Total Time (s) | Ops/sec |
---|---|---|
oj | 0.1342 | 745320 |
marshal | 0.3228 | 309824 |
json | 0.9035 | 110682 |
oj + zlib | 1.8050 | 55401 |
marshal + zlib | 1.8057 | 55381 |
json + zlib | 2.7949 | 35780 |
If opting for OJ, you will need to install the dependency in your project and configure as needed.
Mudis is marginally slower than Rails.cache
by design; it trades raw speed for control, observability, and safety.
10000 iterations of 1MB, Marshal (to match MemoryStore default), compression ON
Operation | Rails.cache | Mudis | Delta |
---|---|---|---|
Write | 2.139 ms/op | 2.417 ms/op | +0.278 ms |
Read | 0.007 ms/op | 0.810 ms/op | +0.803 ms |
For context: a typical database query or HTTP call takes 10–50ms. A difference of less than 1ms per operation is negligible for most apps.
Mudis includes features that MemoryStore doesn’t:
Feature | Mudis | Rails.cache (MemoryStore) |
---|---|---|
Per-key TTL expiry | ✅ | ⚠️ on access |
True LRU eviction | ✅ | ❌ |
Hard memory limits | ✅ | ❌ |
Value compression | ✅ | ❌ |
Thread safety | ✅ Bucket-level mutexes | ✅ Global mutex |
Observability | ✅ | ❌ |
Namespacing | ✅ | ❌ Manual scoping |
It will be down to the developer to decide if a fraction of a millisecond is worth
10000 iterations of 1MB, Marshal (to match MemoryStore default), compression OFF (to match MemoryStore default)
Operation | Rails.cache | Mudis | Delta |
---|---|---|---|
Write | 2.342 ms/op | 0.501 ms/op | −1.841 ms |
Read | 0.007 ms/op | 0.011 ms/op | +0.004 ms |
With compression disabled, Mudis writes significanty faster and reads are virtually identical. Optimisation and configuration of Mudis will be determined by your individual needs.
Don’t forget to stop the expiry thread when your app exits:
at_exit { Mudis.stop_expiry_thread }
rails new mudis-server --api
cd mudis-server
config/initializers/mudis.rb
Rails.application.routes.draw do
get "/cache/:key", to: "cache#show"
post "/cache/:key", to: "cache#write"
delete "/cache/:key", to: "cache#delete"
get "/metrics", to: "cache#metrics"
end
cache_controller
(with optional per caller/consumer namespace)class CacheController < ApplicationController
def show
key = params[:key]
ns = params[:namespace]
value = Mudis.read(key, namespace: ns)
if value.nil?
render json: { error: "not found" }, status: :not_found
else
render json: { value: value }
end
end
def write
key = params[:key]
ns = params[:namespace]
val = params[:value]
ttl = params[:expires_in]&.to_i
Mudis.write(key, val, expires_in: ttl, namespace: ns)
render json: { status: "written", key: key }
end
def delete
key = params[:key]
ns = params[:namespace]
Mudis.delete(key, namespace: ns)
render json: { status: "deleted" }
end
def metrics
render json: Mudis.metrics
end
end
curl http://localhost:3000/cache/foo
curl -X POST http://localhost:3000/cache/foo -d 'value=bar&expires_in=60'
curl http://localhost:3000/metrics
# Write with namespace
curl -X POST "http://localhost:3000/cache/foo?namespace=orders" \
-d "value=123&expires_in=60"
# Read from namespace
curl "http://localhost:3000/cache/foo?namespace=orders"
# Delete from namespace
curl -X DELETE "http://localhost:3000/cache/foo?namespace=orders"
Mudis is intended to be a minimal, thread-safe, in-memory cache designed specifically for Ruby applications. It focuses on:
The primary use cases are:
Mudis is not intended to be a general-purpose, distributed caching platform. You are, however, welcome to build on top of Mudis if you want its functionality in such projects. E.g.,
MIT License © kiebor81
For issues, suggestions, or feedback, please open a GitHub issue
FAQs
Unknown package
We found that mudis demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
A clarification on our recent research investigating 60 malicious Ruby gems.
Security News
ESLint now supports parallel linting with a new --concurrency flag, delivering major speed gains and closing a 10-year-old feature request.
Research
/Security News
A malicious Go module posing as an SSH brute forcer exfiltrates stolen credentials to a Telegram bot controlled by a Russian-speaking threat actor.