msgpack-ruby packs Ruby objects into bytes without a JSON detour
MessagePack implementation for Ruby / msgpack.org[Ruby]
At a glance
- What is it?
- msgpack-ruby is the Ruby implementation of the MessagePack binary format, with a C extension for speed, streaming packers and unpackers, and an extension type system for objects that JSON has no place for.
- Who is it for?
- msgpack-ruby fits a service that already has a MessagePack peer on the other end, whether that peer is Redis, a mobile client, a Fluentd agent or another Ruby process, and it is the right choice when payload size and encoding cost matter more than human readability of the wire format.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 12 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Small integers and short strings are where the byte savings come from
MessagePack is described as an efficient binary serialization format that lets you exchange data among multiple languages like JSON but faster and smaller. The README gives the mechanism plainly: small integers such as flags or error codes are encoded into a single byte, and typical short strings require only an extra byte in addition to the strings themselves.
That is a different bargain from JSON. A JSON array of small integers carries a delimiter and a comma for every element, and every string is quoted and escaped. The MessagePack encoding for an array header plus three small integers is three bytes of payload, as the README's own example shows, where [1,2,3].to_msgpack produces the three byte string "\x93\x01\x02\x03". The leading byte is the array header for three elements and the rest are the values themselves.
The README frames the choice in terms of a wish: if you ever wanted JSON for convenience, for instance storing an image with metadata, but could not use it for technical reasons such as binary data, size or speed, MessagePack is the replacement. The interesting case is not small integers, it is binary payloads, where JSON needs base64 and MessagePack does not.
Symbols and Time both need a registration before they survive a round trip
Two Ruby types that people expect to round trip do not, and the README is upfront about both.
Symbols are serialized as strings by default. The packed form of :symbol is "\xA6symbol" and unpacking it yields the string "symbol". To keep symbols distinct, you register an extension type for them:
MessagePack::DefaultFactory.register_type(0x00, Symbol)
# symbols now survive round trips
packed = :symbol.to_msgpack # => "\xc7\x06\x00symbol"
MessagePack.unpack(packed) # => :symbolThe extension type for symbols is configurable like any other extension type, and you can redefine Symbol#to_msgpack_ext to prevent symbols from being written at all. The README shows a version that raises, which is a useful guard for a service that treats any symbol in a payload as a bug.
Time is a similar story with more steps. MessagePack has a timestamp extension type, but it is not registered by default, so a plain pack drops the distinction between a Time and whatever the default representation is. Registering it needs a packer and unpacker pair:
MessagePack::DefaultFactory.register_type(
MessagePack::Timestamp::TYPE, # or just -1
Time,
packer: MessagePack::Time::Packer,
unpacker: MessagePack::Time::Unpacker
)The consequence for adoption is straightforward. A default install of msgpack-ruby loses type information for symbols and does not round trip Time. If your payloads carry either, registering types is part of the setup, not an optimisation you can defer.
register_type and Factory are how a custom class crosses the wire
Packer and Unpacker support the MessagePack extension type mechanism, which is the answer to the question JSON cannot answer well. A custom class is registered against a type identifier, and the packer and unpacker then agree on how to encode and decode it:
# register how to serialize custom class at first
pk = MessagePack::Packer.new(io)
pk.register_type(0x01, MyClass1, :to_msgpack_ext) # equal to pk.register_type(0x01, MyClass1)
pk.register_type(0x02, MyClass2){|obj| obj.how_to_serialize() } # blocks also available
# almost same API for unpacker
uk = MessagePack::Unpacker.new()
uk.register_type(0x01, MyClass1, :from_msgpack_ext)
uk.register_type(0x02){|data| MyClass2.create_from_serialized_data(data) }MessagePack::Factory exists so that a packer and an unpacker can share the same set of extension types, and MessagePack::DefaultFactory is the factory that MessagePack.pack and MessagePack.unpack refer to, which is why a type registered on the default factory applies process wide.
The recursive option is the part worth understanding. A packer lambda normally receives one object and returns bytes, but with recursive: true the packer is handed the packer itself, so it can write several values in sequence:
factory.register_type(
0x01,
Point,
packer: ->(point, packer) {
packer.write(point.x)
packer.write(point.y)
},
unpacker: ->(unpacker) {
x = unpacker.read
y = unpacker.read
Point.new(x, y)
},
recursive: true,
)The price of that flexibility is that extension payloads are opaque to any peer that does not know the same registration. A peer speaking plain MessagePack will read the extension bytes as an unknown type, so the extension registry has to be agreed on both sides before the format is safe to ship.
Packer and Unpacker stream instead of building one big byte string
The one-shot API is MessagePack.pack for serialization and MessagePack.unpack for deserialization, and for small payloads there is little reason to go further. For anything larger, the streaming API keeps memory flat, because the packer writes into an IO and the unpacker pulls objects out of it as they arrive.
The packer exposes a chainable writing interface:
# serialize a 2-element array [e1, e2]
pk = MessagePack::Packer.new(io)
pk.write_array_header(2).write(e1).write(e2).flushWriting the array header before the elements is the key detail. The header declares the element count so the reader knows how much to expect, and the flush pushes the bytes to the underlying IO, which matters when the target is a socket rather than a buffer.
The unpacker comes in two flavours. The first iterates objects out of an IO:
# deserialize objects from an IO
u = MessagePack::Unpacker.new(io)
u.each do |obj|
# ...
endThe second is event driven and is the one to reach for under EventMachine or any reactor where data arrives in arbitrary chunks:
# event-driven deserialization
def on_read(data)
@u ||= MessagePack::Unpacker.new
@u.feed_each(data) {|obj|
# ...
}
endThe feed_each form is the practical difference between streaming and not. A socket read does not align with object boundaries, so the unpacker buffers partial data internally until it has a complete object and then yields it.
Pooling exists because creating a packer or unpacker is expensive
The README states plainly that creating Packer and Unpacker objects is expensive and that reusing them is preferable for performance. MessagePack::Factory#pool makes that easier, and the pattern is straightforward: keep one factory, register the types on it, and pull packers and unpackers from it as needed.
This matters in a different way from most allocation advice, because the cost here is not the object but the setup it carries. A packer holds state for registered extension types and buffers output; a fresh one starts from nothing. In a request path that serializes thousands of small objects, the per-object allocation of a packer dominates the actual encoding work.
The pattern also composes with the factory idea. A pooled factory still owns one registration list, so type registration happens once at boot rather than per request, and the pool hands out packers that already know about your custom classes.
One caveat belongs here: the README does not document a pool size, a reset guarantee, or thread safety rules for pooled objects. If your service handles concurrent requests, whether a pooled packer can be shared across threads or must be checked out per thread is exactly the kind of detail to confirm in the API reference at ruby.msgpack.org before relying on it.
Installing the gem, or building the native extension from a checkout
For most projects the gem is the whole story. The README gives three routes, and the first two need no build tooling of your own. With Bundler, add the dependency to the Gemfile:
# Gemfile
gem 'msgpack'Or install it straight from RubyGems:
gem install msgpackThe third route is for working on the library itself or for platforms without a prebuilt gem. The repository has an ext/ directory and a msgpack.gemspec, so the gem contains a native extension rather than pure Ruby, and the README describes building it from a checked-out msgpack-ruby repository:
bundle
rake
gem install --local pkg/msgpackThe Rakefile at the root of the repository drives that build, and the resulting gem file lands in pkg/. If your deployment image builds from source, this is the path you will use, and it means a C toolchain has to exist in the build stage. The repository also carries spec/, bench/, bin/, doclib/ and a ChangeLog, which tells you where the tests, the benchmarks and the API documentation sources live.
A first use is two lines:
require 'msgpack'
msg = [1,2,3].to_msgpack #=> "\x93\x01\x02\x03"
MessagePack.unpack(msg) #=> [1,2,3]To move data to and from disk, the README writes it with File.binwrite and reads it back with File.binread, which matters because the payload is binary and a text mode read would corrupt it on platforms that translate line endings.
Where msgpack-ruby earns its place: caches, queues and mobile clients
The README lists the use cases it has in mind, and they are all cases where both ends of the wire are under your control. A REST API returning MessagePack, built on Rails with RABL. Storing objects serialized by msgpack in memcached or Redis, including through Redis EVAL scripts, which the README notes support msgpack. Uploading data from mobile devices such as smartphones, with the Objective-C and Java implementations named as the other side. Designing a portable protocol to talk to embedded devices, with Fluentd named as a log collector that uses msgpack for its log format, which the README notes is JSON compatible in practice. And exchanging objects between software components written in different languages.
That last group is the strongest argument for the format and also its main risk. MessagePack earns its keep when components in different languages need one compact encoding, and the library ecosystem around it is broad enough that a peer is likely to exist. The risk is the same breadth seen from the other side: a payload that mixes plain MessagePack with extension types only your Ruby process registers will decode into something useless on the peer, and nobody sees that failure until a field is missing at runtime.
For a service with a browser or a generic HTTP client on the other end, the format is the wrong choice. There is no readable fallback and no self describing schema, so debugging a payload means decoding it with the same tool that produced it.
Apache-2.0 licensed, moving since 2026, but with no tagged releases
The project is Apache-2.0 licensed, with the LICENSE file at the root of the repository, and its homepage is msgpack.org. The repository is not archived and the last push was on 2026-10-11, so the source is being worked on rather than left alone.
The release story is the odd part. The repository has no GitHub releases at all, so there is no tag to pin in a Gemfile and no release notes to skim on the GitHub page. Changes are recorded in the ChangeLog file that sits beside the README, and the versioning is handled through the gem versions published to RubyGems rather than through GitHub release objects.
Portability is a stated goal with a clear boundary. The README says msgpack-ruby should run on x86, ARM, PowerPC, SPARC and other CPU architectures, and that it works with MRI (CRuby) and Rubinius, adding that patches to improve portability are welcomed. In practice that means the native extension is the part to watch when you add a new target architecture, since the gem is not pure Ruby and a platform without a matching build will fail at install rather than at runtime.
One concrete upgrade consideration follows from all this. A project that pins msgpack in the Gemfile with a strict version will not see the commits landing after the last push, while a project that floats the version may pick up a change with no changelog entry in the release notes, so the ChangeLog in the repository is the artifact to read when a decode behaves unexpectedly.
Editorial conclusion
msgpack-ruby fits a service that already has a MessagePack peer on the other end, whether that peer is Redis, a mobile client, a Fluentd agent or another Ruby process, and it is the right choice when payload size and encoding cost matter more than human readability of the wire format. It is the wrong tool for a public API whose consumers are browsers and curl, since the format is binary by design, and it is the wrong tool for mixed Ruby and JavaScript services that need one format understood by both. Before adopting, decide whether you want symbols and Time preserved, because MessagePack::DefaultFactory does not register those types until you do it, and check whether your deployment can compile the native extension in ext/ rather than relying on a prebuilt gem.
Frequently asked questions
How do I install msgpack-ruby in a Ruby project?
Add gem 'msgpack' to your Gemfile and install with Bundler, or run gem install msgpack from RubyGems. The gem ships a native extension in ext/, so a build toolchain is needed when installing from source.
How do I serialize Ruby symbols and preserve them on unpack?
Symbols are serialized as strings by default. Register the symbol extension type with MessagePack::DefaultFactory.register_type(0x00, Symbol), after which :symbol.to_msgpack produces "\xc7\x06\x00symbol" and unpacking returns the symbol. Redefining Symbol#to_msgpack_ext lets you forbid symbol serialization entirely.
Does msgpack-ruby round trip Time objects by default?
No. The MessagePack timestamp extension type exists but is not registered by default, so you register it yourself on MessagePack::DefaultFactory with the packer and unpacker from MessagePack::Time, passing MessagePack::Timestamp::TYPE or just -1 as the type id.
What is the difference between MessagePack.pack and MessagePack::Packer?
MessagePack.pack serializes an object in one call, while MessagePack::Packer writes into an IO and lets you stream, for example with pk.write_array_header(2).write(e1).write(e2).flush. The Unpacker counterpart yields objects as they arrive, either with each over an IO or with feed_each for event driven code.
How do I avoid creating a new packer for every message?
The README states that creating Packer and Unpacker objects is expensive and recommends reusing them, and MessagePack::Factory#pool makes that easier. A pooled factory also keeps one extension type registration list for the whole process.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/msgpack-msgpack-ruby)Was this useful?