Boundaries without a network hop  / field guide
Practitioner field guide · 27 September 2026

Boundaries without a network hop: ten years of Shopify, read from its own artefacts

Shopify spent a decade refusing to buy isolation with process boundaries, and bought it instead in build-time checks, in a language runtime it forks, and in a WebAssembly sandbox that replaced an mruby one. This guide reconstructs that decade from repositories, release notes and registry timestamps, and gives an architect the four places a boundary can live, the condition that moves it, and the failure classes each location brings with it.

30 graded artefacts 50 ledger rows 2 published sandbox failures Evidence through September 2026 Read: 17 min
01

The territory

One application, thousands of daily changes, and code written by people you cannot trust, all inside a single process family. Where do you put the boundary when you have decided not to put it on the network?

25s→6.5s
Boot time of the Shopify platform with compile and path caching
869 KB
Smallest statically linked JavaScript Wasm module, against 1 to 16 KB dynamically linked
523
Archived repositories in the Shopify organisation, of more than 1,200
Rust 1.85
Toolchain now required to build CRuby with the compiler Shopify wrote for it

State the problem without naming a technology and it stops being a Ruby story. A single large application is changed every day by teams who do not read each other's code, and some of the logic that has to run inside the request is written by third parties with commercial incentives and no operational relationship to you. Both halves need boundaries. The industry's default answer for the first half is a network hop, and for the second half a separate execution environment somewhere else. Shopify took neither default, and the public record of what it took instead is unusually complete, because almost all of it is tooling, and tooling gets published.

What the artefacts show is a decade of moving the boundary downwards rather than outwards. Module boundaries went into a static checker that runs in continuous integration (packwerk, first released 23 September 2020). Interface boundaries went into a type checker consumed as a fork (Shopify/sorbet) fed by a generator that manufactures the signatures the checker cannot infer (Tapioca). Performance went into the language itself: a patched CRuby per upstream release, then two compilers written in Rust and pushed upstream. And the boundary around other people's code moved from a seccomp-confined child process running a second Ruby implementation (the Enterprise Script Service) to WebAssembly modules executed under a deliberately deterministic WASI context.

The surprise sits at the end of the runtime line. Shopify got YJIT into CRuby, watched Rails 7.2 turn it on by default for everyone, and then started a second compiler from scratch. Ruby 4.0.0's own release notes say of the replacement that it "is faster than the interpreter, but not yet as fast as YJIT" and "advise against deploying it in production for now". A company that had already won the argument chose to regress for a release or two rather than keep extending the design it had shipped.

Scope. This guide covers where Shopify put its enforcement boundaries between 2014 and September 2026, and what each location cost. It does not cover the commerce product, the data platform, the machine-learning stack, or any traffic and scale figures, because those live on shopify.engineering, which the research environment could not reach. It also carries no conference talk and no peer-reviewed paper for the same reason. Everything below is a repository artefact, a release note or a registry timestamp, and where a claim is reconstruction rather than report, the sentence says so.

Figure 1 · Four places a boundary can live

Boundary needed between
two pieces of code

1 - Build time
static check in CI
packwerk, Sorbet

2 - Runtime you own
patched CRuby, JIT,
reforking server

3 - OS process
child process, seccomp,
instruction quota

4 - VM sandbox
Wasm module,
deterministic WASI

Stops: accidental coupling
Costs: CI capacity, false negatives

Stops: nothing hostile
Costs: a fork to carry

Stops: hostile code, coarsely
Costs: a process per call

Stops: hostile code, and
nondeterminism
Costs: somebody else's CVEs

Boundary needed between
two pieces of code

1 - Build time
static check in CI
packwerk, Sorbet

2 - Runtime you own
patched CRuby, JIT,
reforking server

3 - OS process
child process, seccomp,
instruction quota

4 - VM sandbox
Wasm module,
deterministic WASI

Stops: accidental coupling
Costs: CI capacity, false negatives

Stops: nothing hostile
Costs: a fork to carry

Stops: hostile code, coarsely
Costs: a process per call

Stops: hostile code, and
nondeterminism
Costs: somebody else's CVEs

Each row is a location for enforcement, with what it can stop and what it costs to run. Shopify occupies all four, and moved work down the list rather than up it. Sources: packwerk usage, ESS, function-runner.
Diagram source
02

How it is actually built

The stack that keeps one large application governable, reconstructed layer by layer from the repositories that implement each layer.

Figure 2 · The boundary stack around one monolith

JSON in, JSON out

Runtime Shopify forks

Shopify/ruby
pshopify line

YJIT, then ZJIT

Build time, in CI

packwerk
+ package_todo

Sorbet fork
fed by Tapioca RBIs

One large Rails app
semian bulkhead per dependency
pitchfork reforking workers

Other people's code
Wasm module,
deterministic WASI

MySQL, Vitess adopter
ghostferry moves rows

JSON in, JSON out

Runtime Shopify forks

Shopify/ruby
pshopify line

YJIT, then ZJIT

Build time, in CI

packwerk
+ package_todo

Sorbet fork
fed by Tapioca RBIs

One large Rails app
semian bulkhead per dependency
pitchfork reforking workers

Other people's code
Wasm module,
deterministic WASI

MySQL, Vitess adopter
ghostferry moves rows

Every box is a published artefact, named in the evidence wall. The dashed box is the only place a request crosses into code Shopify did not write. Sources: rubydex, semian, ghostferry.
Diagram source

Read the stack from the top and the design intent is visible. The first layer is build-time, and its job is to make a boundary that Ruby does not have. Packwerk's own usage document states the gap plainly: "Ruby does not provide a good solution to enforcing boundaries between code." The interesting engineering is not the check, it is the adoption mechanism. Packwerk ships a recorded-violations file, the package todo, so an existing codebase can turn enforcement on while it is still in violation, and the file becomes the debt ledger. Without that, static boundary enforcement is only available to codebases that do not need it yet.

The same layer contains the honest limitation. Packwerk's README states that it is "designed to avoid false positives ... at any cost, and we pay the cost by accepting a small number of false negatives", and that "method calls and objects passed around the application are completely ignored", because it resolves constant references only. That is the architectural fact to carry: this boundary catches structural coupling through constants and is blind to behavioural coupling through arguments. The published remedy is to express the argument types as Sorbet signatures, which are themselves constants, so the two tools compose. Typing then needs signatures for code nobody typed, which is what Tapioca generates from gems, Rails and DSLs, with continuous-integration validation that the generated files are still in sync. A build-time boundary in a dynamic language is therefore not one tool, it is a pipeline with its own freshness problem.

The second layer is the one most organisations would not attempt. Shopify runs a fork of the language: Shopify/ruby is "forked from ruby/ruby" and carries a branch per upstream release, currently v4.0.7-pshopify1, updated 21 September 2026. The public definition repository describes the builds as "API compliant with release Rubies, but with additional backports for bugfixes and performance". The patch line has a naming convention and a rake task to cut the next one, which tells you it is routine rather than heroic. Two compilers were built in that fork and then moved out of it: the archived Shopify/zjit README now reads "We have upstreamed the project into ruby/ruby", and CRuby's own documentation asks ZJIT bug reports to be filed "on the official Ruby bug tracker (or, if you don't want to make an account, on Shopify/ruby)". A subsystem of the reference implementation is triaged in a company fork's issue tracker, where the twelve most recent open issues are all prefixed "ZJIT:".

The runtime layer extends into the process model, which is the part that is easy to miss. A tracing compiler makes memory the scarce resource rather than CPU, so Shopify replaced its web server: pitchfork is "a preforking HTTP server for Rack applications designed to minimize memory usage by maximizing Copy-on-Write performance", and its distinguishing feature is reforking, promoting "a warmed-up worker as the new template from which workers are forked". CRuby's 3.3.0 release notes close the loop from the other side, disabling code garbage collection by default for "Better copy-on-write behavior on servers reforking with Pitchfork". Compiler, garbage collector and process supervisor were tuned as one system, by one company, across two repositories it does not own.

The third layer is failure containment inside the process, and it is the oldest code in this guide. Semian, first published to RubyGems on 26 September 2014, combines a circuit breaker with a bulkhead whose concurrency limit is "coordinated server-wide with SysV semaphores". That detail is the architecture: without a network boundary there is no proxy to count concurrent calls, so the counter lives in kernel-visible shared memory on the host. Toxiproxy, from the same period, exists so the resulting behaviour can be tested by breaking the network in integration tests rather than in production.

The fourth layer is the only one that faces code Shopify did not write, and it was rebuilt end to end during the decade. The first generation is still readable: the Enterprise Script Service is "a thin Ruby API layer that spawns a process, the enterprise_script_engine, to execute an untrusted Ruby script", which "spawns an mruby-engine with seccomp sandboxing" and takes a timeout, instruction quotas and memory limits. Merchant code was a "stripped-down version of Ruby", per the archived Scripts repository. The second generation keeps the contract, JSON in and JSON out, and changes the substrate to WebAssembly: function-runner executes "Wasm Functions intended for the Shopify Functions infrastructure", the Wasm API repository ships a trampoline that "augments Wasm modules to interface with the provider", and deterministic-wasi-ctx removes nondeterminism from the guest's syscalls so that, in its own words, the results of "invoking a function in a Wasm module" can be cached.

Debt ledger, not a gate

Boundary enforcement became adoptable because violations could be recorded rather than fixed first. The package todo file is what lets a ten-year-old codebase turn the check on this quarter.

Artefact: packwerk USAGE.md

Determinism as a feature

The sandbox was not only about safety. Fixing the clock and the random source makes a third party's function replayable, and therefore cacheable, which a seccomp-confined process never was.

Artefact: deterministic-wasi-ctx

A verdict, not a rollout

The deploy boundary is a wrapper whose stated purpose is an answer: krane exists because "in a CI/CD environment, we need a clear, actionable pass/fail result for each deploy", which kubectl apply does not give.

Artefact: krane README

03

The decisions that matter

Four forks in the road, each with the artefact that records the choice and the condition that would move it.

Decision: where does the module boundary get enforced?

Chosen
  • A static check in continuous integration, on constant references, with a recorded-debt file (packwerk, 2020 onwards)
  • Cost is CI capacity and a known blind spot, not a call boundary
Rejected
  • Extracting the package into a service, the industry default
  • No artefact argues against it directly; what the artefacts show is a decade of investment in the alternative and none in service extraction tooling
Flips when
  • The code needs a different deploy cadence or runtime, or must be isolated from people you do not trust, neither of which a constant-reference check can give you

Decision: how do you make a dynamic language fast enough to keep the monolith?

Chosen
  • Write a just-in-time compiler for the reference implementation, in Rust, and upstream it. Ruby 3.1's notes report "up to 22% on railsbench, 39% on liquid-render"
  • Ruby 3.2 declared it no longer experimental after "production workloads for over a year"
Rejected
  • Rewriting the hot library in C. liquid-c did exactly that and measured roughly three times faster parsing, then stopped: last release 24 January 2024, README marked "no longer maintained"
Flips when
  • The cost is concentrated in one library you own, where a C extension pays back immediately. It flips back when the cost is spread across the whole application, or when the extension's behaviour must track the pure-Ruby version forever

Decision: how do you run code written by your customers inside checkout?

Chosen
  • Since 2023, a Wasm module per function, run by the host with a trampoline and a deterministic WASI context; Scripts were given a removal date of 28 August 2025
Rejected
  • Continuing with a second Ruby implementation in a seccomp-confined child process with instruction quotas, which is what ESS and mruby-engine implement
Flips when
  • You need only one guest language and can afford a process per invocation. It flips to Wasm the moment you want several source languages, per-call resource accounting inside the VM, or cacheable results, which need determinism

Decision: fork the dependency, or wait for upstream?

Chosen
  • Fork, and plan the repayment. The language fork carries backports per release; the compilers moved into ruby/ruby; bootsnap's Shopify path now serves rails/bootsnap; Javy's README now reads "A Bytecode Alliance project"
Rejected
  • Keeping the work private and carrying the fork indefinitely, which is what the abandoned artefacts in this corpus look like: liquid-c unmaintained, mruby-engine still public with no successor named
Flips when
  • There is no upstream willing to take the change. Then the fork is permanent and should be budgeted as a product, with a named owner and a patch cadence, rather than as a patch

Figure 3 · Choosing the boundary's location

No

Yes

Yes

No

Structural, constants

Behavioural, arguments

Yes

No

Do you trust
the author of
the code?

Wasm sandbox
+ deterministic WASI
function-runner

Does it need its own
deploy cadence
or runtime?

Separate service
network boundary,
not in this corpus

Is the coupling you fear
structural or
behavioural?

packwerk
+ package_todo

Sorbet signatures
+ Tapioca RBIs

Must results be
cached or replayed?

Remove clock and
randomness from WASI

Stock WASI context

No

Yes

Yes

No

Structural, constants

Behavioural, arguments

Yes

No

Do you trust
the author of
the code?

Wasm sandbox
+ deterministic WASI
function-runner

Does it need its own
deploy cadence
or runtime?

Separate service
network boundary,
not in this corpus

Is the coupling you fear
structural or
behavioural?

packwerk
+ package_todo

Sorbet signatures
+ Tapioca RBIs

Must results be
cached or replayed?

Remove clock and
randomness from WASI

Stock WASI context

The two questions that decide the location are trust and deploy cadence, in that order; performance decides the runtime layer, not the boundary. Terminal nodes name the artefact that implements that answer at Shopify.
Diagram source
DecisionChosenRejectedBecauseEvidence
Module boundariesStatic check on constants, debt fileService extractionRuby offers no boundary mechanism; enforcement had to be adoptable mid-lifepackwerk usage
Interface contractsGradual types with generated signaturesTests aloneStatic boundary checks are blind to arguments unless types make them constantsTapioca
Application speedA JIT compiler in the runtimeC extension per hot libraryCost is spread across the application, not concentrated in one libraryRuby 3.1 notes
Second compilerMethod-based, profile-guided (ZJIT)Extending lazy basic-block versioning furtherUpstream accepted a temporary regression against YJIT to change the designRuby 4.0 notes
Memory of JIT codeReforking server, code GC off by defaultPer-worker JIT memoryCopy-on-write sharing of warmed workers is cheaper than compiling per workerpitchfork
Untrusted codeWasm module, JSON contractmruby in a seccomp child processMultiple guest languages and cacheable, deterministic executiondeterministic-wasi-ctx
Data partitioningAdopt Vitess, own the movement toolBuild the partitioning layerThe scarce capability was moving a tenant's rows with a bounded write freezeghostferry overview
Fork exitUpstream or donateCarry the fork privatelyEvery surviving line in this corpus left for ruby/ruby, rails or a foundationShopify/zjit
04

What broke in production

Two failure classes dominate this corpus: build-time caches that return a plausible wrong answer, and a rented isolation boundary whose defects arrive on somebody else's release schedule. Neither is an incident review, because Shopify publishes none.

Read this before the cards

A search of every issue in the Shopify organisation for postmortem and for the phrase "caused an outage" returned zero results on 27 September 2026. The failure record below is therefore assembled from defect reports filed by adopters and from security advisories published by the projects Shopify depends on. That absence is itself a finding: when a company's architecture is tooling, its incidents are invisible and its dependencies' incidents are not.

Defect report

A precompiled cache stored false and every boot believed it

AssumptionA compile cache keyed on file size and modification time is safe, and a stale entry will simply miss.
What happenedWhen a YAML file was edited without changing its byte size, bs_precompile read the same file descriptor twice without rewinding; the second read hit end of file, yielded an empty string, and YAML parsed that as false, which was then written into the cache as the document.
Blast radiusSilent and unbounded in principle: any consumer of that file sees false instead of its contents until the size changes. The guard existed already on the read path, bs_fetch, and was missing on the precompile path.
FixMirror the existing guard, opened 4 September 2026 and closed through pull request 573.
Design ruleAudit every write path into a cache, not just the read path. A cache that can return a well-typed wrong value is worse than one that crashes, and size plus mtime is not a content key.
Defect report

Loading a gem from cache corrupted the heap, five per cent of the time

AssumptionA boot-time optimisation is a development convenience that can be upgraded like any other minor dependency.
What happenedAfter an upgrade from 1.20.1 to 1.21.1, the reporter saw a "corrupted double-linked list" abort inside load_from_binary while loading a gem, in roughly five per cent of continuous-integration runs, under concurrency from parallel test processes.
Blast radiusIntermittent and unreproducible by design: the failure depends on timing and on which instruction sequences are cached, so it will not appear in a single verification run.
FixNone recorded in the thread at the time of checking.
Design ruleAnything that deserialises machine-level artefacts into your process is memory-unsafe surface. Pin it, roll it like a runtime component, and keep a switch that disables the cache in one deploy.
Advisory

The sandbox that runs merchant code let guests out of the filesystem

AssumptionIsolation obtained from a mature WebAssembly runtime is a property you own once you have configured it.
What happenedWasmtime's own advisory reports that the wasmtime-wasi filesystem implementation "contained a bug which allowed guests to escape their filesystem sandbox and read/write external files and directories", triggered by paths or symlinks with trailing slashes. Severity High.
Blast radiusFour supported release lines were patched on the same day, 20 August 2026: 24.0.13, 36.0.14, 46.0.3 and 47.0.4. Nine further advisories were published against the same project between 15 June and 24 September 2026, including guest-driven host memory exhaustion.
FixUpgrade, on the upstream project's timetable rather than yours.
Design ruleRented isolation converts a security property into a patch-latency problem. Before adopting it, measure how fast you can move across a major version of the runtime, because the advisory will land on several lines at once and your line may not be the one you are on.
Defect report

The boundary checker quadrupled the memory its adopters needed

AssumptionA static check is cheap, because it only reads code.
What happenedBetween packwerk 1.4 and 2.0 an adopter measured maximum resident set size rising from 83,560 KB to 334,968 KB, with major page faults going from zero to 5,862, and attributed it to the new version loading the Rails application in each of its parallel processes.
Blast radiusContinuous integration, not production: the reporter concluded "we will need to increase the resource class of our CI nodes".
FixNot recorded in the thread. The issue was closed in December 2022, a year after it was opened.
Design ruleWhen you move a boundary into the build, your build becomes a production system with a capacity plan. Budget the checker's resource profile before you mandate it, and measure it on the largest package, not the sample app.

Figure 4 · How a cache returns a well-typed lie

CacheYAML parserFile descriptorbs_precompileDeveloperCacheYAML parserFile descriptorbs_precompileDeveloperbs_fetch guards this case,bs_precompile did notedit file, same byte size, newmtimeread sourcecontentsread again, no rewindempty string at EOFparse empty stringfalsestore false as the document
CacheYAML parserFile descriptorbs_precompileDeveloperCacheYAML parserFile descriptorbs_precompileDeveloperbs_fetch guards this case,bs_precompile did notedit file, same byte size, newmtimeread sourcecontentsread again, no rewindempty string at EOFparse empty stringfalsestore false as the document
The bug is not the cache, it is that one of two entry paths lacked the guard the other one had. Reconstructed from bootsnap issue 572.
Diagram source

Figure 5 · What happens to software Shopify builds for itself

solves a problem the ecosystem has not

reference implementation takes it (YJIT, ZJIT)

another org maintains it (bootsnap to rails)

a foundation maintains it (Javy)

unmaintained notice, no successor (liquid-c)

repository archived as historical

still installable, still in lockfiles

Built

Upstreamed

Transferred

Donated

Abandoned

Risk

solves a problem the ecosystem has not

reference implementation takes it (YJIT, ZJIT)

another org maintains it (bootsnap to rails)

a foundation maintains it (Javy)

unmaintained notice, no successor (liquid-c)

repository archived as historical

still installable, still in lockfiles

Built

Upstreamed

Transferred

Donated

Abandoned

Risk

Four exits, observed in this corpus. The healthy ones move maintenance to an upstream; the unhealthy one leaves a present-tense README with no successor named. Sources: Shopify/zjit, bootsnap, liquid-c.
Diagram source
05

Numbers you can plan against

Everything quantitative in this corpus, with the date it was measured and who measured it. There are fewer first-party production figures than a reader would like, and the reason is in the callout below.

MetricValueAtContextAs ofSource
Boot time, large monolith25s to 6.5sShopifyRoughly 75% faster; 75% of the gain from path caching, 25% from compile cachesREADME, checked 2026-09bootsnap
Boot time, third party6s to 3sDiscourseReported in the same README, one machineREADME, checked 2026-09bootsnap
JIT gain, first release22% / 39%Ruby corerailsbench and liquid-render, the second being a template workload2021-12Ruby 3.1
JIT default call threshold30, or 120Ruby coreRaised automatically above 40,000 instruction sequences, so the default is application-size dependent2023-12Ruby 3.3
JIT memory budget64 MiB, then 128 MiBRuby coreExecutable memory in 3.2, unified memory limit in 3.42022-12, 2024-12Ruby 3.4
Latency claim for the framework default15% to 25%RailsDescribed in the pull request as "public reports", not a measurement by its author2023-11Rails 49947
Template engine, C against Ruby29.5 to 90.7 i/sShopifyParsing; rendering 89.4 to 163.9 i/s, on ruby-3.0.2, benchmark in the repositoryREADME, last release 2024-01liquid-c
Wasm module size869 KB, or 1 to 16 KBBytecode AllianceStatic linking against dynamic linking of the JavaScript runtimeREADME, checked 2026-09Javy
Boundary checker memory83,560 to 334,968 KBAdopterMaximum resident set size, packwerk 1.4 against 2.0, Ruby 3.0.22021-12packwerk 164
Type checker release cadence1,934 versionsSorbetFirst 2018-06-02, latest 2026-09-25; the adoption cost is continuous upgrade2026-09-27RubyGems API
New Rust toolkit cadence124 versions in 13 monthsShopifyrubydex, first 2025-08-19, latest 2026-09-022026-09-27RubyGems API
Archived repositories523ShopifyOf more than 1,200 public repositories in the organisation2026-09-27GitHub
Sandbox advisories10 in 15 weeksWasmtimePublished between 2026-06-15 and 2026-09-24, one rated High2026-09-27Advisory index
Data move downtimeSeconds to minutesShopifyThe write freeze between stopping writes and repointing the application, per the tool's own documentationDocs, checked 2026-09ghostferry
Published client driftLast release 2024-11-04ShopifyShopifyAPI for Python, 78 releases since 2011, while the repository showed activity in August 20262026-09-27PyPI API
Extension-point sunset28 August 2025ShopifyAnnounced removal date for Scripts; the repository was archived 3 September 2024, eleven months earlier2024-09shopify-scripts
Read these carefully

The boot-time figures are the only first-party Shopify production numbers in this corpus, and they are README figures without a date, so treat them as an order of magnitude rather than a measurement. The Rails latency range is explicitly a summary of other people's reports. The liquid-c benchmark was run on Ruby 3.0.2, which predates the compiler that arguably replaced it, so it does not tell you what a C extension is worth today. No traffic, shard count, request rate or cost figure appears anywhere above, because those are published on an engineering blog this research could not reach; plan against your own measurements, not against an absent number.

06

The evidence wall

Every source behind this page, graded. There is no engineering-blog, talk or paper tier here, and that is a property of the research environment rather than of the topic: those hosts were blocked, so the guide was built from repositories and registries only.

Figure 6 · The decade in four eras

2014 to 2017 - resilience and deploys
semian, toxiproxy, liquid-c,
kubernetes-deploy, bootsnap

2018 to 2020 - build-time boundaries
Sorbet and Tapioca, packwerk,
listed as a Vitess adopter

2021 to 2023 - the runtime itself
YJIT upstreamed and declared stable,
pitchfork, Rails enables it by default

2024 to 2026 - guests and a new toolchain
Scripts retired, Wasm Functions,
rubydex, ZJIT ships in Ruby 4.0

2014 to 2017 - resilience and deploys
semian, toxiproxy, liquid-c,
kubernetes-deploy, bootsnap

2018 to 2020 - build-time boundaries
Sorbet and Tapioca, packwerk,
listed as a Vitess adopter

2021 to 2023 - the runtime itself
YJIT upstreamed and declared stable,
pitchfork, Rails enables it by default

2024 to 2026 - guests and a new toolchain
Scripts retired, Wasm Functions,
rubydex, ZJIT ships in Ruby 4.0

Read left to right and the direction of travel is clear: resilience first, then deployment, then build-time boundaries, then the runtime itself, then the sandbox for other people's code. Dates are first releases or archive notices, all from the ledger.
Diagram source
Postmortem Bytecode Alliance2026-08

Filesystem sandbox escape when paths or symlinks contain trailing slashes

A High-severity advisory against the runtime that executes Shopify Functions, reporting that guests could "escape their filesystem sandbox and read/write external files and directories". Four release lines were patched the same day.

Carry forwardRenting isolation converts a security property into a patch-latency commitment; know how fast you can cross a major version.
github.com/bytecodealliance/wasmtime/security/advisories/GHSA-vqjp-4c8c-hfgg
Postmortem Bytecode Alliance2026-09

Wasmtime security advisory index

Ten advisories between 15 June and 24 September 2026, including host panics through filesystem datetime overflow and guest-driven host memory exhaustion. This is the operational cost profile of a sandbox you did not write.

Carry forwardCount advisories per quarter before adopting a VM as a trust boundary; that number is your upgrade cadence.
github.com/bytecodealliance/wasmtime/security/advisories
Decision record Ruby core2026-09

doc/jit/zjit.md, the second compiler's design document

Describes ZJIT as "a method-based just-in-time (JIT) compiler" that "uses profile information from the interpreter to guide optimization", and directs bug reports either to the Ruby tracker or to Shopify's fork.

Carry forwardWhen a vendor's fork is named as an acceptable bug tracker for an upstream subsystem, the upstream has accepted a governance dependency; know which one you are filing into.
raw.githubusercontent.com/ruby/ruby/master/doc/jit/zjit.md
Decision record Ruby core2026-09

doc/jit/yjit.md, the first compiler's design document

States the architecture, "It lazily compiles code using a Basic Block Versioning (BBV) architecture", and is candid that the compiler "will use more memory than the Ruby interpreter", with a whole section on reducing it.

Carry forwardA tracing or versioning compiler trades memory for CPU; the capacity question it raises is resident set size per worker, not throughput.
raw.githubusercontent.com/ruby/ruby/master/doc/jit/yjit.md
Decision record Shopify2026-09

packwerk USAGE.md, including the package todo file

Names the gap it fills, "Ruby does not provide a good solution to enforcing boundaries between code", and documents recording existing violations so enforcement can begin before the codebase complies.

Carry forwardShip the debt ledger with the check. Enforcement without a recorded-violations mechanism is only adoptable on new code.
raw.githubusercontent.com/Shopify/packwerk/main/USAGE.md
Decision record Rails2023-11

Pull request 49947, enable YJIT by default if running Ruby 3.3+

The moment one company's compiler became the default runtime configuration for the framework. The description cites "public reports of 15-25% latency improvements", and the review argues about where in boot the compiler should be enabled.

Carry forwardA framework default is the fastest distribution channel for runtime work, and the slowest to reverse. Read the initialiser ordering argument before copying the pattern.
github.com/rails/rails/pull/49947
Decision record Shopify2026-09

ghostferry technical overview

Documents that "Ghostferry mandates that you stop writes to the dataset you are copying at a stage of execution called cutover", and prices the resulting downtime "on the order of seconds to minutes". A TLA+ specification accompanies the design.

Carry forwardA bounded, honest write freeze beats an unbounded promise of zero downtime; specify the freeze and rehearse it.
raw.githubusercontent.com/Shopify/ghostferry/main/docs/technicaloverview.md
Source Shopify2026-09

Shopify/ruby, branch listing

The language fork, "forked from ruby/ruby", carrying a patch branch per upstream release, currently v4.0.7-pshopify1, alongside a dozen active compiler branches.

Carry forwardIf you fork a runtime, name the patch line after the upstream version and cut it mechanically; that is what makes the fork survivable.
github.com/Shopify/ruby/branches/all
Source Shopify2026-09

shopify-ruby-definitions

Build definitions for "Rubies we use at Shopify", described as "API compliant with release Rubies, but with additional backports for bugfixes and performance", with a rake task that cuts the next patch level.

Carry forwardA fork is a loan. Publishing the build definition is how you keep the interest visible to everyone who has to reproduce it.
github.com/Shopify/ruby-definitions
Source RubyGems2023-10

shopify-ruby-definitions release history

Five versions, ending at 1.4.0 on 11 October 2023, while the repository and the fork it describes remained active into September 2026. The public gem stopped being the way the patched runtime is distributed.

Carry forwardDate a dependency from its registry, not its repository. An active repository with a dead registry means the real artefact is internal.
rubygems.org/api/v1/versions/shopify-ruby-definitions.json
Source Shopify2025-05

Shopify/zjit, archived

The whole README is a redirection: "We have upstreamed the project into ruby/ruby in the zjit directory. This repository is mostly for historical interest."

Carry forwardThe healthiest end state for infrastructure you built is an archive notice pointing at an upstream that now carries it.
raw.githubusercontent.com/Shopify/zjit/master/README.md
Source Ruby core2025-12

NEWS for Ruby 4.0.0

Introduces ZJIT and states that it "is faster than the interpreter, but not yet as fast as YJIT", advising against production deployment, with production readiness targeted at 4.1. Building it requires Rust 1.85.0 or later.

Carry forwardA second-generation rewrite regresses before it wins. Keep the first generation shipping and default until the replacement passes it on your workload.
raw.githubusercontent.com/ruby/ruby/master/doc/NEWS/NEWS-4.0.0.md
Source Ruby core2022-12

NEWS for Ruby 3.2.0

"YJIT is no longer experimental", justified by having "been tested on production workloads for over a year", and from this release building the reference implementation with the compiler requires a Rust toolchain.

Carry forwardGetting your work into a runtime everyone uses also imports your build dependencies into their pipeline; expect that to be the contested part.
raw.githubusercontent.com/ruby/ruby/master/doc/NEWS/NEWS-3.2.0.md
Source Ruby core2023-12

NEWS for Ruby 3.3.0

The compiler's call threshold is raised automatically "from 30 to 120 when the application has more than 40,000 ISEQs", and code garbage collection is disabled by default for "Better copy-on-write behavior on servers reforking with Pitchfork".

Carry forwardRuntime defaults that key off application size are a signal: the tuning that matters is yours, and the vendor is telling you where the cliff is.
raw.githubusercontent.com/ruby/ruby/master/doc/NEWS/NEWS-3.3.0.md
Source Shopify2026-01

pitchfork

A reforking Rack server that periodically promotes "a warmed-up worker as the new template from which workers are forked", explicitly to raise the proportion of shared memory.

Carry forwardIf you adopt a JIT, revisit the process model in the same quarter. Compiled code is only affordable if it is shared.
raw.githubusercontent.com/Shopify/pitchfork/master/README.md
Source Shopify2026-08

packwerk README

Documents the deliberate trade in the checker's precision: avoid false positives "at any cost", accept false negatives, and ignore method calls and arguments entirely because only constant references are resolved.

Carry forwardWrite down your boundary checker's blind spot next to the rule it enforces, or teams will trust it for coupling it cannot see.
raw.githubusercontent.com/Shopify/packwerk/main/README.md
Source Shopify2026-09

Tapioca

Generates the type signatures a checker "cannot otherwise see", from gems, Rails and metaprogramming-heavy DSLs, and validates in continuous integration that the generated files are still in sync.

Carry forwardGradual typing in a dynamic language is a generated-artefact pipeline with a staleness problem; budget the generator, not just the checker.
raw.githubusercontent.com/Shopify/tapioca/main/README.md
Source Shopify2026-09

rubydex

A Rust "high-performance static analysis toolkit for the Ruby language", intended as "a solid foundation to power a variety of tools, such as type checkers, linters, language servers", with 124 gem releases since August 2025.

Carry forwardWhen several build-time checks each parse the whole codebase, the next investment is one shared index, not a faster checker.
raw.githubusercontent.com/Shopify/rubydex/main/README.md
Source Shopify2024-09

shopify-scripts, archived

Merchant customisations "written in a stripped-down version of Ruby", archived on 3 September 2024 with a deprecation notice giving a removal date of 28 August 2025.

Carry forwardAn extension point is a public contract. Retiring it needs a dated removal, a live replacement, and archive of the examples people copied from.
github.com/Shopify/shopify-scripts
Source Shopify2026-09

Enterprise Script Service and mruby-engine

The first-generation sandbox: "a thin Ruby API layer that spawns a process, the enterprise_script_engine, to execute an untrusted Ruby script", using an mruby engine under seccomp, with timeout, instruction quotas and memory limits.

Carry forwardUntrusted code needs three limits, not one: wall clock, instructions and memory. The substrate changes; those three do not.
github.com/Shopify/ess
Source Shopify2026-09

deterministic-wasi-ctx

A WASI context where "a provided WASI function will always return the same series of results for the same series of invocations", offered explicitly so that the results of invoking a guest function can be cached.

Carry forwardDeterminism is an architectural feature, not a purity concern: it is what lets you memoise, replay and test a third party's logic.
raw.githubusercontent.com/Shopify/deterministic-wasi-ctx/main/README.md
Source Bytecode Alliance2026-09

Javy, formerly Shopify/javy

A JavaScript to WebAssembly toolchain, now "A Bytecode Alliance project", whose README publishes the size economics: 1 to 16 KB with dynamic linking against at least 869 KB when statically linked.

Carry forwardWith one guest module per tenant, module size is a storage and cold-start budget; dynamic linking of the guest runtime is the lever.
raw.githubusercontent.com/Shopify/javy/main/README.md
Source Shopify2026-09

shopify-function-wasm-api and function-runner

The host side of the guest contract: a provider, a core type crate, an API, and a trampoline that "augments Wasm modules to interface with the provider" and handles memory sharing. The runner executes a module against a JSON input file.

Carry forwardGive third parties a local runner with the same contract as production. It is the cheapest support channel you will ever build.
raw.githubusercontent.com/Shopify/function-runner/main/README.md
Source Shopify2026-09

semian

Bulkheads and circuit breakers inside the application process, with concurrency "coordinated server-wide with SysV semaphores" because there is no proxy in the path to count calls.

Carry forwardWithout a network boundary you still need a concurrency limiter; put the counter somewhere every worker on the host can see.
raw.githubusercontent.com/Shopify/semian/main/README.md
Source Shopify2024-01

liquid-c, unmaintained

A partial C reimplementation of the template language, with a repository benchmark showing roughly three times faster parsing, now carrying the notice "This project is no longer maintained", last released January 2024.

Carry forwardA second implementation of your own library is a permanent parity obligation. Prefer making the one implementation faster if you can reach the runtime.
raw.githubusercontent.com/Shopify/liquid-c/main/README.md
Case study Rails2026-09

bootsnap README, with the platform's boot numbers

Reports the Shopify platform booting "about 75% faster, dropping from around 25s to 6.5s", and attributes about 25% of the gain to compile caches and 75% to path caching. The Shopify repository path now serves the Rails organisation's copy.

Carry forwardBefore optimising compilation, measure path resolution. In the one published split, most of the boot win was filesystem lookups.
raw.githubusercontent.com/Shopify/bootsnap/main/README.md
Source Vitess2026-09

Vitess ADOPTERS.md

Lists Shopify among known adopters of Vitess, alongside Slack, Square, GitHub and others. The partitioning layer was adopted; the tool that moves rows between instances was built.

Carry forwardSplit the question: adopt the sharding middleware, own the migration tooling, because the migration is where your tenancy model lives.
raw.githubusercontent.com/vitessio/vitess/main/ADOPTERS.md
Source Go module proxy2026-09

ghostferry module versions

The Go module proxy lists v1.2.0, v1.3.0 and v1.3.1 for the data mover, while the repository's tag list carries release and canary tags dated September 2026. The public module line is a subset of what the team actually cuts.

Carry forwardFor a Go dependency, read the module proxy and the tag list together; a quiet proxy with busy tags means the maintainer ships to itself first.
proxy.golang.org/github.com/!shopify/ghostferry/@v/list
Source PyPI2024-11

ShopifyAPI for Python, release history

Seventy-eight releases from 23 August 2011 to version 12.7.0 on 4 November 2024, while the repository behind it showed activity in August 2026. The same registry-against-repository drift appears here as in the patched-Ruby definitions.

Carry forwardJudge an official client's support from its registry, not its commit graph; the two diverge quietly and only one of them is what you install.
pypi.org/pypi/ShopifyAPI/json
Source Shopify2026-01

krane, formerly kubernetes-deploy

A deploy wrapper whose stated purpose is a verdict: "in a CI/CD environment, we need a clear, actionable pass/fail result for each deploy". The gem history dates the Kubernetes move to January 2017 and the rename to November 2019.

Carry forwardDeployment tooling earns its keep by answering "did it work", not by applying manifests. Design the verdict first.
raw.githubusercontent.com/Shopify/krane/main/README.md
07

Build a miniature, then productionise it

Six rungs. The first three can be done in an evening each on an existing application; the last three are where the design decisions in this guide become real.

Draw one boundary, record the debt

Pick two directories in an application you already run, declare them as packages, turn on dependency enforcement, and commit the recorded-violations file without fixing anything.

Done when: continuous integration is green with violations recorded, and a new violation fails the build.  Teaches: enforcement is adoptable only if existing breakage can be parked explicitly.

Find the coupling the checker cannot see

Write a method that takes an object from the other package as an argument and returns it, using no constant reference. Confirm the boundary check passes, then add a type signature and watch it fail.

Done when: you have one example of each, committed as a test.  Teaches: structural and behavioural coupling need different instruments.

Price the runtime, in memory not milliseconds

Run a representative workload with the compiler off, on, and on with the memory limit halved. Record resident set size per worker and latency at the same request rate.

Done when: you can state the megabytes per worker your latency win costs.  Teaches: a just-in-time compiler is a memory purchase, which is why the process model changes with it.

Run somebody else's code, twice

Compile a small function to WebAssembly, run it against a JSON input with a stock host context, then again with the clock and random source fixed. Feed the same input twice and compare the outputs byte for byte.

Done when: the deterministic run is byte-identical and the stock run is not.  Teaches: why a sandbox built for caching removes nondeterminism at the syscall boundary, not in the guest.

Break the dependency, on purpose

Put a bulkhead and a circuit breaker in front of one slow dependency, then use a fault-injecting proxy to add latency rather than errors, which is the case that consumes workers. Observe queue depth and the breaker's state.

Done when: a saturated dependency produces fast failures and a stable worker pool.  Teaches: slow is the failure mode that needs a boundary; down mostly takes care of itself.

Move a tenant, with the freeze in the runbook

Copy one tenant's rows between two database instances with change capture running, verify, freeze writes, cut over, unfreeze. Time the freeze and write it into the runbook as a number.

Done when: the freeze duration is measured and the verifier reports no mismatch.  Teaches: a stated, rehearsed freeze is cheaper than an unbounded claim of zero downtime, and the verifier is the part you cannot skip.

08

Keep hunting

These are the queries that produced this page, in an environment that could reach code hosts and package registries but no engineering blogs. They work on any company with a large public estate.

Dating decisions from artefacts

  • https://github.com/orgs/<org>/repositories?q=archived%3Atrue
  • https://rubygems.org/api/v1/versions/<gem>.json
  • https://crates.io/api/v1/crates/<crate>
  • https://registry.npmjs.org/<package> and read the time map
  • https://github.com/<org>/<repo>/branches/all for a fork's patch line

Finding the argument and the failure

  • org:<org> postmortem in the issue search, and treat zero as a finding
  • org:<org> "caused an outage"
  • repo:<org>/<repo> slow OR performance for adopter-measured ceilings
  • https://github.com/<org>/<repo>/security/advisories for a dependency's failure record
  • raw.githubusercontent.com/<org>/<repo>/<branch>/doc/ for design documents that never became blog posts
09

References

  1. Shopify, Shopify/ruby branch listing GitHub. Checked 2026-09-27.
  2. Shopify, shopify-ruby-definitions GitHub. Checked 2026-09-27.
  3. RubyGems, shopify-ruby-definitions versions Last release 2023-10-11. Checked 2026-09-27.
  4. Shopify, Shopify/ruby issue tracker GitHub. Checked 2026-09-27.
  5. Ruby core, ZJIT documentation ruby/ruby master. Checked 2026-09-27.
  6. Ruby core, YJIT documentation ruby/ruby master. Checked 2026-09-27.
  7. Ruby core, NEWS for Ruby 3.1.0 Released December 2021. Checked 2026-09-27.
  8. Ruby core, NEWS for Ruby 3.2.0 Released December 2022. Checked 2026-09-27.
  9. Ruby core, NEWS for Ruby 3.3.0 Released December 2023. Checked 2026-09-27.
  10. Ruby core, NEWS for Ruby 3.4.0 Released December 2024. Checked 2026-09-27.
  11. Ruby core, NEWS for Ruby 4.0.0 Released December 2025. Checked 2026-09-27.
  12. Shopify, Shopify/zjit README, archived Archived 2025. Checked 2026-09-27.
  13. Rails, pull request 49947, enable YJIT by default Merged 2023-11-07. Checked 2026-09-27.
  14. Shopify, pitchfork README Last release 2026-01-28. Checked 2026-09-27.
  15. Shopify, packwerk README Last release 2026-08-26. Checked 2026-09-27.
  16. Shopify, packwerk usage guide GitHub. Checked 2026-09-27.
  17. Shopify, packwerk issue 164 Opened 2021-12-09. Checked 2026-09-27.
  18. Shopify, Tapioca README Last release 2026-09-15. Checked 2026-09-27.
  19. RubyGems, sorbet versions 1,934 versions to 2026-09-25. Checked 2026-09-27.
  20. Shopify, Shopify/sorbet fork GitHub. Checked 2026-09-27.
  21. Shopify, rubydex README First release 2025-08-19. Checked 2026-09-27.
  22. Shopify, liquid-c README Last release 2024-01-24. Checked 2026-09-27.
  23. Rails, bootsnap README Served from the Shopify path. Checked 2026-09-27.
  24. Rails, bootsnap issue 572 Opened 2026-09-04. Checked 2026-09-27.
  25. Rails, bootsnap issue 529 GitHub. Checked 2026-09-27.
  26. Shopify, shopify-scripts, archived Archived 2024-09-03. Checked 2026-09-27.
  27. Shopify, Enterprise Script Service GitHub. Checked 2026-09-27.
  28. Shopify, mruby-engine GitHub. Checked 2026-09-27.
  29. Bytecode Alliance, Javy README Served from the Shopify path. Checked 2026-09-27.
  30. crates.io, javy crate Created 2023-05-17. Checked 2026-09-27.
  31. Shopify, function-runner README GitHub. Checked 2026-09-27.
  32. Shopify, Shopify Function Wasm API GitHub. Checked 2026-09-27.
  33. Shopify, deterministic-wasi-ctx GitHub. Checked 2026-09-27.
  34. Bytecode Alliance, Wasmtime filesystem sandbox escape Published 2026-08-20. Checked 2026-09-27.
  35. Bytecode Alliance, Wasmtime advisories Ten advisories 2026-06-15 to 2026-09-24. Checked 2026-09-27.
  36. Shopify, semian README Last release 2026-09-07. Checked 2026-09-27.
  37. Shopify, krane README Last release 2026-01-08. Checked 2026-09-27.
  38. RubyGems, kubernetes-deploy versions First release 2017-01-18. Checked 2026-09-27.
  39. Shopify, ghostferry technical overview GitHub. Checked 2026-09-27.
  40. Shopify, ghostferry tags Latest 2026-09-24. Checked 2026-09-27.
  41. Vitess, ADOPTERS.md GitHub. Checked 2026-09-27.
  42. Shopify, archived repositories listing 523 archived. Checked 2026-09-27.
  43. npm, @shopify/hydrogen release history First release 2021-08-16. Checked 2026-09-27.
  44. RubyGems, rubydex versions 124 versions to 2026-09-02. Checked 2026-09-27.
  45. RubyGems, packwerk versions First release 2020-09-23. Checked 2026-09-27.
  46. Go module proxy, ghostferry versions Checked 2026-09-27.
  47. PyPI, ShopifyAPI release history Last release 2024-11-04. Checked 2026-09-27.