Marshal’s Hidden Cost
Shopify’s Rails monolith, like most Rails applications, relies on caching at nearly every layer—page rendering, database queries, and external data retrieval all lean on it. The cache interface is simple: Rails.cache exposes read and write, and every backend—whether Memcached, Redis, file, or memory—uses the same underlying serialization pair: Marshal.load and Marshal.dump.
Marshal is Ruby’s most permissive serializer. It can turn nearly any object into a binary blob and restore it later. That flexibility makes it ideal for caching in a framework like Rails, which touches everything from actions to partials to query results. But it comes with a serious, often invisible, risk.
A few years ago, that risk materialized at Shopify. A developer refactored some beta flag classes in our core monolith. The change passed review and CI, but once deployed, it provoked a flood of exceptions and an incident. The fix was a rapid rollback. The uncomfortable truth: the code was correct. The problem was that old code, still running during the deploy, started reading cached entries written by new code—and those entries contained class names and methods it didn’t recognize.
Marshal serializes an object’s class along with its data. When classes change, cached bytes can reference stale structures. In this case, beta flags were cached widely, making the collision inevitable and broad.
As part of Shopify’s Ruby and Rails Infrastructure team, I joined the follow-up. We weighed two traditional mitigations, and both were unacceptable. We could reduce what we cache, but that undermines the point of a cache. Or we could ask developers to change code less—which conflicts with our mission to keep the codebase clean through refactoring.
We chose a third path: replace Marshal with a serialization format we control. The format we settled on was MessagePack—a compact binary serializer with stricter typing and less magic than Marshal. This series, based on a RailsConf talk of the same name, digs into how Marshal works and how we built a safer caching layer. Part one focuses on Marshal from the inside out.
What Rails Caching Documentation Doesn't Tell You
Rails' caching guides explain how to use the cache, not what to put in it. The low-level section claims Rails caching “works great for storing any kind of information.” That overpromises. What’s missing is an explanation of what’s safe to store under a deployment model where code evolves.
In Rails 7, some progress was made. Jean Boussier, also on Shopify’s Ruby and Rails Infrastructure team, optimized cache space allocation, making ActiveSupport::Cache::Entry more efficient. The space savings help, but it doesn’t change the fundamental equation—all cache backends still default to Marshal for serialization.
Inside the Marshal Payload
To replace Marshal, we needed to understand what it does. Good documentation on marshal.c, Ruby’s source of truth for this serializer, is scarce. So we turned directly to the source—and to the binary output of a simple example.
Take a Post record with a title column. Passing that instance through Marshal.dump yields a roughly 1,600-byte string. The payload is dense: constants for Rails classes like ActiveRecord, ActiveModel, and ActiveSupport; instance variables (recognizable by the leading @); and the title value itself, “Caching Without Marshal,” appearing three times.
That blob, handed to Marshal.load, reproduces the exact original object. You can do this anytime, from any process running the same Ruby code. That’s possible because Marshal recursively crawls an object and all its references, encoding everything—including the class—into the output.
The constants at the top of marshal.c define the building blocks. MARSHAL_MAJOR and MARSHAL_MINOR appear first in every payload and effectively function as a fixed header.
marshal.cThe remaining types fall into groups. “Atomic” types stand alone: nil, booleans, numbers, floats, symbols, classes, and modules. “Composite” types contain others: arrays, hashes, structs, and objects—plus two surprises: strings and regexes. Both are optimized in Ruby’s runtime and therefore get special encoding treatment. Toward the end of the list are specialized types with less obvious roles, which we’ll examine later.
How Objects Serialize
The workhorse type for object serialization is TYPE_OBJECT, noted as o in the payload.
Decoding the Post object, you first get the Marshal version (0408), then the object marker (6f). Next follows the class name symbol—a colon (3a), a length byte (09), and the ASCII string. Small numbers use an optimized form, so 09 translates to length 4. After that, an integer counts the instance variables, each paired with its name and value. Since each value can itself be an object with its own instance variables, payloads can grow unpredictably—and unlike most binary formats, Marshal uses recursion, not flat tables.
Core Types Handle Their Own Details
Because String, Regex, Array, and Hash are implemented natively by Ruby rather than as regular objects, Marshal finds them special in a different way. They still can carry instance variables, as this unusual—but legal—Ruby shows:
With instance variables attached, Marshal uses TYPE_IVAR, a wrapper that adds variable names and values on top of the core type encoding. It applies equally to hashes, arrays, and regexes.
Circularity by Design
Circular references are another Marshal specialty. A record’s associations often point back to the record itself. Marshal handles this without issue, thanks to the link type (TYPE_LINK in marshal.c).

A minimal self-referencing array—a = []; a << a—serializes in just a few bytes. The payload starts with the array type (5b) and its length (06 for a single element in optimized byte form). The self-referencing element is represented by @ (40) for the link type, followed by the index 00, pointing back to the array's first slot. The decode process reconstructs the cycle cleanly.
Marshal gives you circular safety for free. But it also gives up control—class versioning, custom object shape evolution, and safe-by-default degradation aren’t part of its contract. That’s what we needed to build ourselves. Part two examines how, using a stricter—but safer—serialization format and the changes required to cache arbitrary objects.
Subclassing Core Types
Ruby implements several core classes—String, Regex, Array, and Hash—in a special way, and Marshal treats them differently from ordinary objects. This distinction matters when you subclass one of these classes and try to serialize an instance.
When you create an instance of such a subclass, it behaves like the parent type but remains an instance of the subclass. Marshal preserves this correctly on serialization: it records the actual subclass name alongside the serialized data, allowing the object to be restored with its original class upon deserialization.
Marshal accomplishes this through a dedicated type, TYPE_UCLASS. In addition to the usual data for the underlying type—hash data, for instance—TYPE_UCLASS appends the class name. The same mechanism handles subclasses of strings, arrays, and regexes.
Why Marshal's Details Matter
This inner working of Marshal might seem academic, but it has practical consequences. Any Rails application depends on Marshal whether you realize it or not, particularly for caching.
If you decide to remove Marshal from your application—as Shopify did—these encoding behaviors are exactly what breaks. Before making such a migration, it's essential to understand how to replicate each of Marshal's features in whatever format replaces it.
Shopify's migration used MessagePack. Replacing Marshal's capabilities in that format required re-implementing features like circular reference handling and core type subclass support, along with a custom algorithm for encoding records and their associations. The details of that migration and encoding strategy are covered in the next part of this series.



