Ruby 4 introduced Ruby::Box, and I built a minimal setup to run shadow execution against a Rack app—meaning the same request is evaluated a second time “behind the scenes” with an alternate implementation.
- Real (primary path): run the production app normally and return the real response as-is
- Shadow (secondary path): run alternate logic inside Ruby::Box using the same input and log only the diffs
- In my “shadow universe,” I intentionally play with rules like “Y2K time,” “fixed
rand,” “gyaru-style I18n,” and “/coffeereturns 418” (just for visibility)
Ruby::Box docs: https://docs.ruby-lang.org/en/master/Ruby/Box.html
https://github.com/geeknees/ruby_box_shadow_universe
Background: Why run shadow execution?
When you change production behavior, there’s always a lingering fear:
- Did I break compatibility (status / headers / body)?
- Did exceptions or latency increase?
- Does a bug only trigger on specific paths?
The goal of shadow execution is to evaluate “new logic” with the same inputs without changing the production response, and to create a state where you can observe differences safely.
Why Ruby::Box (vs “just extracting code”)?
Shadow execution itself can be done by extracting logic into another class.
In practice, though, once you start experimenting on the shadow side, you often want to do things like:
- Apply monkey patches only on the shadow side (Time/Random/I18n/HTTP, etc.)
- Allow “risky dependencies” or “experimental code” only in shadow
- Try behavior/version differences without polluting the main app
The problem is that within a single process, constants, autoload, top-level definitions, and monkey patches can easily leak and contaminate the main world.
Ruby::Box provides a model of separation “per box” (you require/load files inside the box so their definitions live in that world).
Whether it’s “okay” to overwrite Time.now or rand in the first place is a separate discussion 😉
High-level architecture
A Rack middleware keeps the Real → Response path intact, runs Shadow asynchronously, and logs diffs.
How to run it
Ruby::Box needs to be enabled at startup via an environment variable:
bundle install
RUBY_BOX=1 bundle exec rackup -p 9292
Implementation (key points)
1) Rack middleware: return Real, run Shadow in the background
- Return the result of
@app.call(env)as-is - On the shadow side, load
shadow_logic.rbinto aRuby::Box, then callShadowLogic.call - Compare the “shadow response” vs the “real response” and log diffs
For readability, diff collection is extracted into a small helper (status / content-type / body bytes, etc.):
def add_diff(diff, label, before, after)
return if before == after
diff << "#{label}: #{before.inspect} -> #{after.inspect}"
end
Reference:
https://github.com/geeknees/ruby_box_shadow_universe/blob/main/shadow_box_middleware.rb#L87
2) Shadow logic: keep “shadow world” definitions inside a file
What you do in shadow is up to you, but common “experiment” patterns include:
- Trying transforms / corrections / validations only in shadow
- Adding extra observability data only in shadow
- Applying alternate rules only in shadow (e.g. certain paths return 418)
In this repo, I include an intentionally obvious example: changing Time/Random/I18n rules only in shadow, so differences are easy to observe.
Example logs: diffs only
Shadow results are not returned to the client. Only differences are logged (which is easier to treat as observability).
127.0.0.1 - - [29/Dec/2025:15:23:27 +0900] "GET /hello HTTP/1.1" 200 - 0.0029
[shadow_box] 🌚 alternate universe detected: x-shadow-universe: nil -> "Y2K+RAND2+GYARU", body(bytes): 11 -> 123
[shadow_box] x-shadow-universe Y2K+RAND2+GYARU
[shadow_box] outside: Time.now=2025-12-29T15:23:27+09:00 rand=13
[shadow_box] inside : (computed per-request in alt_body)
[shadow_box] --- shadow report ---
req: GET /hello
at: 1999-12-31T23:59:59+09:00
rand: 2
say: こんちわ〜⭐️
original bytes: 11
Reference:
https://github.com/geeknees/ruby_box_shadow_universe/blob/main/shadow_box_middleware.rb#L79
Operational notes / caveats
0) It’s still experimental
At runtime you’ll see a warning like:
warning: Ruby::Box is experimental, and the behavior may change in the future!
1) Ruby::Box is not a sandbox
Ruby::Box is not OS-level isolation.
It won’t prevent external I/O (network, files, processes). For safety, shadow logic should be designed to be side-effect free whenever possible.
2) Shadow execution adds cost
It increases per-request work. In practice, shadow is often used with:
- Sampling (only a percentage of requests)
- Path-based targeting
- Time limits (timeouts)
- Async execution (threads, job queue, etc.)
Summary
- A Rack middleware can provide a solid base for shadow execution
- Ruby::Box makes it easier to define/override behavior only in the shadow world
- Because you keep the production response unchanged, you can introduce changes progressively while observing diffs

Top comments (0)