SHOPIFY, 2014–2026  / field guide
Practitioner field guide · 11 September 2026

Where Shopify refused to rewrite, and where it rewrote three times

One company, one decade, two opposite policies. On the runtime it can change, Shopify has not migrated once since 2014; it bought the constraint and made it faster. On the runtime it cannot change, its storefront framework has moved foundations three times in five years. This guide reconstructs both from the repository record and extracts the test that separates them, so an architect can decide which of the two situations they are in before they choose between evolving a system and replacing it.

36 sources, all fetched 16 repositories 4 recorded failure threads Evidence through September 2026 Read: 34 min
01

The territory

The problem, stated without naming a language: when a system you cannot leave is too slow or too tangled, do you improve it or escape it? Shopify has answered that question in public, in both directions, for ten years.

25s → 6.5s
Shopify platform boot time after the caching work that became bootsnap
3m30 → 20s
Boundary check across the monolith once results were cached by file content
16.7%
Worker capacity spent re-opening circuit breakers at default MySQL settings
3
Foundations under the storefront framework since 2021, the last one bought outright

Every large engineering organisation eventually faces the same fork. A shared substrate, usually a language runtime and a framework, is holding the product back. The obvious moves are to escape it, by splitting the system into services that can each pick their own stack, or to endure it. Shopify took a third option on the backend and has held it for a decade: keep one codebase, keep one language, and spend the money on the substrate itself. The repository record shows what that cost and what it bought.

It also shows the counter-example, inside the same company and the same decade. Shopify's headless storefront stack has been rebuilt on three different foundations since 2021. Between the first and the second it went so far as to acquire the team behind the framework it adopted, and the churn continued anyway. Comparing the two arcs is the point of this guide, because the difference between them is not maturity or taste. It is leverage: on the backend Shopify can change the thing it depends on, and on the storefront it cannot.

The finding that was not expected. In January 2026 Shopify archived Ruvy, its attempt to run Ruby inside the WebAssembly sandbox it gives merchants, with a plain statement in the README: Ruvy is "not currently compatible with Shopify Functions" because the modules it produces exceed "the maximum size of Wasm modules supported by Shopify Functions". The company that spent the decade making Ruby fast enough to keep could not fit Ruby inside the extension platform it built for everyone else. A sandbox is not a smaller copy of your runtime. It is a different runtime with a different admission rule, and your own language has to apply like everyone else.

What this guide covers. The public repository record of Shopify and the projects it funds, between 2014 and September 2026: resiliency libraries, the modularisation tooling, the Ruby JIT work, the database driver change, the Kubernetes deploy tooling, the WebAssembly extension toolchain, and the storefront framework lineage. What it deliberately does not cover: anything Shopify never published. The shard router, the pod topology, the Storefront Renderer, the data platform and the peak-traffic figures are all absent, and so is every Shopify engineering blog post, conference talk and incident report, because this session's network policy reached github.com and nothing else. That limit is stated in full in the evidence wall, and it shapes what can honestly be claimed here: this is the record of what was open sourced, which is a biased sample of what was built.

Figure 1 · Three layers, three policies

Substrate Shopify can change:
the Ruby runtime, Rails itself

Invest and stay.
JIT funded and upstreamed,
driver adapter upstreamed

Substrate Shopify shares:
Kubernetes, the MySQL client

Adopt and wrap thin.
krane over kubectl,
Trilogy from GitHub

Substrate Shopify only consumes:
the React ecosystem

Migrate, repeatedly.
Three foundations
under the storefront since 2021

Substrate Shopify can change:
the Ruby runtime, Rails itself

Invest and stay.
JIT funded and upstreamed,
driver adapter upstreamed

Substrate Shopify shares:
Kubernetes, the MySQL client

Adopt and wrap thin.
krane over kubectl,
Trilogy from GitHub

Substrate Shopify only consumes:
the React ecosystem

Migrate, repeatedly.
Three foundations
under the storefront since 2021

The policy follows the leverage, not the technology. Where Shopify can ship a change to the substrate it invests and stays; where it can only consume the substrate it wraps thinly and migrates. Reconstructed from the repositories cited in sections 2 and 3.
Diagram source
The test

Before choosing between improving a dependency and replacing it, answer one question: can you ship a change to it? Not "could we fork it", but can a person on your payroll land a change in the artefact everyone else consumes, on a schedule you influence. If yes, improvement is usually cheaper than escape, and it compounds. If no, you are a tenant, and a tenant should keep the wrapper thin and budget for moving house.

02

How it is actually built

The shape that emerges from sixteen repositories: one codebase, partitioned by tenant rather than by function, with the boundaries enforced at build time and the failure containment sitting inside the process.

The conventional reading of a large commerce platform is a service mesh with a dozen domains behind it. The artefacts do not describe that system. They describe a single Rails application, partitioned horizontally so that a tenant lives on a shard, with four mechanisms bolted to it that would ordinarily be provided by a network: a static boundary checker instead of service interfaces, in-process bulkheads instead of a sidecar, an opt-in blob cache instead of a read-path service, and a WebAssembly sandbox instead of a plugin API.

The clearest evidence for the tenant partitioning is in an unglamorous place. Shopify's job-iteration gem, in production since May 2017, exists to make background jobs interruptible and resumable, and the README gives the reason in one sentence: "At Shopify, we also use it to interrupt workloads safely when moving tenants between shards and move shards between regions." A company only builds a checkpointing job framework if moving a tenant between shards is a routine operation. The unit of isolation is the shop, not the service.

Boundaries inside the code are enforced by Packwerk, which groups files into packages, gives each package a public surface, and fails a check when one package reaches into another's internals. The decisive design choice is not the checker, it is the todo file: existing violations are written into package_todo.yml and, in the words of the usage document, "worked off over time" rather than blocking. That converts modularisation from a migration into a ratchet, which is the only form in which a live monolith can accept it. The cost is a build-time one, and it is real: checking the whole monolith took three and a half minutes until an outside contributor added content-addressed caching in 2022 and brought it under twenty seconds.

Failure containment sits inside the worker process. Semian has been in production since October 2014 and does two things: it trips a circuit breaker when a resource starts failing, and it limits how many workers on a host may wait on that resource at once. In a services architecture, both of those usually live in a sidecar or a mesh. Putting them in the process costs nothing per request and requires no extra hop, and it only works because the unit of concurrency is a worker process with many siblings on the same host. Its sibling tool, Toxiproxy, has been in every development and test environment since the same month, which is the part most organisations skip: the containment is exercised on laptops, not only in a game day.

Reads are served through IdentityCache, a blob cache that is explicitly opt-in and explicitly lossy. The README does not claim correctness; it states the budget: "IdentityCache is never going to be 100% consistent, since cache invalidations can be lost", then lists the ways they are lost, from a write that skips an after_commit callback to a process dying between the database commit and the invalidation. Naming your own inconsistency in the README is a design decision, and a better one than an unstated guarantee that operators will assume is total.

Merchant code does not run in that process at all. It is compiled to WebAssembly, and the toolchain for that, Javy, now sits under the Bytecode Alliance rather than in Shopify's own organisation. Its README quantifies the constraint that matters: modules in "the 1 to 16 KB range with use of dynamic linking", against "at least 869 KB" when linked statically. A platform that runs untrusted extensions on every request is buying cold-start time in kilobytes, which is why the language admission rule is a size rule, and why Ruby did not pass it.

Figure 2 · Reference architecture reconstructed from the repositories

Merchant code

One pod: a slice of tenants

MySQL via Trilogy

checkpoint, resume

invoke per request

gates the deploy

Build time

Packwerk check
package boundaries

package_todo.yml
recorded violations

Rails worker processes
one codebase

Semian
circuit breaker + bulkhead

IdentityCache
opt-in blob cache

job-iteration
resumable jobs

Wasm module
1 to 16 KB

Shard database

Memcached

Merchant code

One pod: a slice of tenants

MySQL via Trilogy

checkpoint, resume

invoke per request

gates the deploy

Build time

Packwerk check
package boundaries

package_todo.yml
recorded violations

Rails worker processes
one codebase

Semian
circuit breaker + bulkhead

IdentityCache
opt-in blob cache

job-iteration
resumable jobs

Wasm module
1 to 16 KB

Shard database

Memcached

Four mechanisms that a services architecture would place on the network sit inside the process or the build here. Reconstructed from packwerk, semian, identity_cache, job-iteration and javy. Dashed boxes are build-time, not runtime.
Diagram source

Partitioning is by tenant

Shards hold shops, and shops move between shards as a routine operation. The job framework is built around that fact rather than around throughput.

Stated at: job-iteration

Boundaries are a ratchet, not a migration

Violations are recorded and paid down. A checker that blocks on day one gets turned off on day two, which is the failure mode this design avoids.

Stated at: packwerk USAGE.md

The deploy tool answers one question

Krane exists because kubectl apply "leaves its users with some burning questions: What just happened? Did it work?" It adds a verdict, not a platform.

Stated at: krane

03

The decisions that matter

Six forks where the repository record shows both the road taken and the reason, plus the condition that would flip each one.

The most expensive decision in the set is also the least visible, because its artefact is not a Shopify repository at all. Shopify's response to Ruby being slow was to fund a JIT compiler inside CRuby. YJIT began in a Shopify repository, which now carries a one-line epitaph, "YJIT has been merged upstream, and is now an official part of Ruby 3.1+", and the work moved into the language. By the Ruby 3.2 release notes the verdict was in: "YJIT is no longer experimental", having "been tested on production workloads for over a year and proven to be quite stable". Note what that sentence implies. The production workload doing the testing was Shopify's, which is the deal a company makes when it improves its own constraint: it ships the risk first and the benefit to everyone.

The same decision is now being made a second time, and the record is honest about the price. Ruby 4.0 ships ZJIT, a method-based compiler that uses interpreter profiles, as experimental, with the note that "ZJIT is faster than the interpreter, but not yet as fast as YJIT" and a stated goal of becoming "production-ready in Ruby 4.1". Four years after the first compiler stabilised, the second one is slower than the one it is meant to replace. Buying your constraint is not a one-off purchase; it is a standing team.

Decision: enforce module boundaries in the build, or extract services?

Chosen
  • Static package boundaries in one codebase (Packwerk)
  • Existing violations recorded in a todo file and paid down over time
  • Call sites stay in-process, so no new network failure modes are created
Rejected
  • Extracting the same boundaries as services
  • The tool's own framing is dependency cost, not deployment: "This knowledge is a dependency that raises the cost of change"
Flips when
  • The boundary needs to be operational as well as logical: independent deploy cadence, independent scaling, separate on-call
  • A static checker cannot give you any of those three, and no amount of tuning will make it

Decision: make the runtime faster, or move the hot paths off it?

Chosen
  • Fund a JIT inside CRuby, written in Rust, and upstream it
  • Benchmark it continuously in public (yjit-metrics)
  • One improvement reaches every line of the application at once
Rejected
  • Rewriting hot services in a faster language
  • That path pays per service, forever, and splits the codebase the rest of the strategy depends on
Flips when
  • You cannot employ or influence the runtime's maintainers, so your patch is a fork
  • Or the ceiling is algorithmic rather than interpretive, in which case a JIT moves nothing

Decision: build the replacement database driver, or adopt one?

Chosen
  • Adopt Trilogy, which GitHub had already open sourced and was running
  • Run it in the monolith first, then upstream the adapter to Rails (merged April 2023)
Rejected
  • Maintaining a private adapter, or writing a third MySQL client
  • The pull request says plainly that two production applications were already on it
Flips when
  • The component has no second production user. One operator's driver is a dependency; two operators' driver is a standard
  • Also flips when your resiliency layer cannot classify the new component's errors, which is what stalled semian PR #420 for six months
DecisionChosenRejectedBecauseFlips whenEvidence
Contain a slow dependencyIn-process circuit breaker and bulkheadSidecar or mesh-level ejectionNo extra hop; many worker processes per host make host-local semaphores meaningfulFew processes per host, or the resource is shared across languagessemian
Exercise failureFault injection in every dev and test environment since 2014Periodic game days in stagingContainment that is not exercised daily is theatreNever; the cheaper variant is to run the same tool in fewer environmentstoxiproxy
Ship to KubernetesA thin verdict layer over kubectlAn in-house orchestrator or PaaSThe gap was "Did it work?", not schedulingDeploys must coordinate several clusters plus data migration; then the wrapper becomes a workflow enginekrane
Run extension codeWebAssembly modules, size-cappedA plugin API inside the host runtimeUntrusted code on the request path needs an admission rule that is mechanicalYour own language has no small Wasm target, at which point the sandbox excludes you tooruvy
Own a tool long-termDonate it once it is no longer differentiatingKeep it in the company organisationbootsnap now lives under rails, javy under the Bytecode AllianceThe tool still encodes something you need to keep changing on your own schedulebootsnap
Storefront frameworkAdopt a third-party framework, then acquire its teamContinue the in-house frameworkThe in-house one was superseded; its repository was moved out and marked legacyAcquisition does not buy the ecosystem underneath, so the churn continues regardlesshydrogen-v1

The storefront row deserves its own paragraph, because it is the one that contradicts the rest. Hydrogen's first generation was Shopify's own React framework; it now sits in a separate repository behind a legacy banner pointing at its successor, while still cutting releases on a branch named v1.x-2022-07 as late as March 2025. The second generation was built on Remix, whose team Shopify acquired. Remix's data APIs were then moved down into React Router, a decision recorded in the open and dated 29 July 2022, and React Router's later conventions shipped with an adapter package specifically to carry Remix users across. The current Shopify Hydrogen README describes itself as "designed to dovetail with React Router", and the Remix repository now opens with "Welcome to Remix 3!" and a set of principles that includes "Model-First Development (optimizing for LLMs)". Owning the framework did not stop the movement, because the movement was coming from the layer below it.

Figure 3 · Three foundations under the storefront in five years

superseded, repo marked legacy

data APIs moved down, ADR 0005, July 2022

framework renamed, discussion 10333

Hydrogen v1
Shopify's own React framework

Hydrogen 2
built on Remix

React Router v7
routes.ts, ADR 0011

Remix 3
away from React

Legacy branch v1.x-2022-07
still releasing in March 2025

superseded, repo marked legacy

data APIs moved down, ADR 0005, July 2022

framework renamed, discussion 10333

Hydrogen v1
Shopify's own React framework

Hydrogen 2
built on Remix

React Router v7
routes.ts, ADR 0011

Remix 3
away from React

Legacy branch v1.x-2022-07
still releasing in March 2025

Every arrow is a migration somebody's storefront had to perform, and the second one happened after Shopify acquired the framework's team. Blue boxes are the generations Shopify owned outright. Reconstructed from hydrogen-v1, react-router decision 0005, decision 0011 and the Remix repository.
Diagram source

Figure 4 · Which branch of the fork you are on

yes

no

interpretive

algorithmic

yes

no

yes

no

Can someone you pay
land a change upstream?

Is the ceiling interpretive
or algorithmic?

Does the boundary need
its own deploy and on-call?

Is there a second
production user?

Fund the substrate.
Upstream, benchmark in public,
staff it permanently

Fix the algorithm first.
A compiler will not move it

Extract the service.
Accept the network failure modes

Enforce the boundary in the build.
Record violations, pay them down

Adopt it and wrap thin.
Budget a migration every few years

Build the minimum yourself,
and plan to donate it later

yes

no

interpretive

algorithmic

yes

no

yes

no

Can someone you pay
land a change upstream?

Is the ceiling interpretive
or algorithmic?

Does the boundary need
its own deploy and on-call?

Is there a second
production user?

Fund the substrate.
Upstream, benchmark in public,
staff it permanently

Fix the algorithm first.
A compiler will not move it

Extract the service.
Accept the network failure modes

Enforce the boundary in the build.
Record violations, pay them down

Adopt it and wrap thin.
Budget a migration every few years

Build the minimum yourself,
and plan to donate it later

Terminal nodes are actions, not verdicts. The first question is about leverage, not about whether the dependency is good. Derived from the decisions above.
Diagram source
04

What broke, and what it teaches

Four failure threads recorded in issue trackers rather than in incident reports. Read the blast-radius rows carefully: none of these is a published outage.

An honest caveat before the cards. Shopify does publish incident analysis, but not on a host this session could reach. What follows is the next best public record: engineers inside and outside the company describing a failure mode precisely enough to be actionable, with numbers where they gave them. Three of the four threads are still open, which is itself the most transferable fact in this section. A known failure mode with an unmerged fix is the normal state of production software, and the design rule has to work in that state.

Postmortem

The recovery eats the capacity it is recovering

AssumptionA circuit breaker protects capacity while a dependency is down.
What happenedWhen the circuit goes half-open, every worker that can acquire a ticket attempts the transition back to closed. If the resource is still unhealthy, each of those workers blocks for the half-open timeout. The probing is the load.
Blast radiusMeasured, not estimated: at the default MySQL settings of a 1.0s half-open timeout and a 5.0s error timeout, "16.7% of capacity goes toward re-opening the circuit", or 8.3% with bulkheads enabled. Filed July 2019; still open in September 2026.
FixProposed in PR #247: throttle the bulkhead to one ticket while open and to the success threshold while half-open. Not merged; a reviewer notes it trades recovery latency, since in the worst case "n - 1 workers will wait at least an additional error_timeout seconds after recovery".
Design ruleSize the probe, not just the breaker. Recovery traffic is a budget you must set explicitly: one prober per resource per host is usually right, and a breaker that lets every worker probe has simply moved the outage into the recovery path.
Postmortem

A canary deploy silently drops cache invalidations

AssumptionPutting the schema version in the cache key makes a schema change safe, because old and new keys cannot collide.
What happenedThey cannot collide, and that is the problem. During a rollout, processes on the old version fill and invalidate the old namespace while processes on the new version fill the new one. An invalidation issued by an old process never reaches the new namespace, so a stale value survives in a key nobody is invalidating.
Blast radiusNot quantified publicly. The trigger can be as ordinary as changing Rails' ignored_columns, which the maintainer describes as action at a distance. Canary deploys widen the window by design. Filed April 2023, still open.
FixProposed: move the schema hash out of the key and into the value, with a primary schema-agnostic key, fallback keys, and a data_version UUID so a stale fill can be detected after the fact.
Design ruleAny identity you put in a cache key becomes a deploy-time partition. If two versions of your application can run at once, and they always can, then a key that encodes the version guarantees a window where invalidation is one-directional.
Sourceidentity_cache issue #535, April 2023
Postmortem

The optimisation assumes a writable disk, so the container will not boot

AssumptionA cache that makes boot faster can fail open: no cache, slower boot, same outcome.
What happenedOn read-only container images the cache write raised Errno::EACCES during boot, so the process did not start at all. The report is specific about where it hurts: worker processes running from read-only images.
Blast radiusPer-deployment rather than per-request, which is the worst kind of surprise, because it appears when you harden the runtime rather than when you change the application. Reported March 2018, since closed.
FixThe reporter's own framing was the right one: log a warning instead of failing to boot. Degrade to the uncached path.
Design ruleEvery performance cache needs a declared behaviour when its backing store is unavailable, and the default must be to lose the speed rather than the service. Test it by mounting the cache directory read-only in CI.
Sourcebootsnap issue #144, March 2018
Postmortem

The sandbox rejects the host's own language

AssumptionIf merchant extensions run as WebAssembly, any language with a WebAssembly target can be offered to merchants, including the one the platform is written in.
What happenedCompiling Ruby to WebAssembly carries the interpreter with it. The resulting modules exceeded the maximum module size Shopify Functions accepts, so Ruvy could never run on the platform it was built for. The README says so directly, and lists splitting the interpreter into a separate engine module as an idea rather than a plan.
Blast radiusNo production impact; the cost was the road not taken. Merchants writing Functions write JavaScript or Rust, not Ruby. Archived by Shopify on 27 January 2026.
FixNone shipped. Javy, the JavaScript toolchain, stayed and moved to the Bytecode Alliance; the Ruby equivalent was retired.
Design ruleWhen you define an extension platform, write the admission rule as a measurable limit first, then check which languages pass it. Doing it the other way round means discovering years in that your own stack is inadmissible.
SourceShopify/ruvy, archived January 2026

Figure 5 · Why half-open recovery costs capacity

Unhealthy MySQLCircuit stateWorker 2Worker 1Unhealthy MySQLCircuit stateWorker 2Worker 1both blocked forhalf_open_resource_timeout 1.0s1.0s of every 6.0s cycle is spent probing,which is the 16.7% in issueerror_timeout 5.0selapses, state becomeshalf_open1probe query2probe query3still failing4still failing5back to open foranother 5.0s6
Unhealthy MySQLCircuit stateWorker 2Worker 1Unhealthy MySQLCircuit stateWorker 2Worker 1both blocked forhalf_open_resource_timeout 1.0s1.0s of every 6.0s cycle is spent probing,which is the 16.7% in issueerror_timeout 5.0selapses, state becomeshalf_open1probe query2probe query3still failing4still failing5back to open foranother 5.0s6
Each worker that probes a still-unhealthy resource is unavailable for the half-open timeout, so the probe rate and the timeout together set the capacity tax. Reconstructed from semian issue #244.
Diagram source

Figure 6 · The invalidation that never arrives

DatabaseMemcachedProcess on newschemaProcess on oldschemaDatabaseMemcachedProcess on newschemaProcess on oldschema"v2:shop:42" is untouchedand now staleread row1fill key "v2:shop:42"2write row, commit3invalidate key "v1:shop:42"4read key "v2:shop:42"5stale value served6
DatabaseMemcachedProcess on newschemaProcess on oldschemaDatabaseMemcachedProcess on newschemaProcess on oldschema"v2:shop:42" is untouchedand now staleread row1fill key "v2:shop:42"2write row, commit3invalidate key "v1:shop:42"4read key "v2:shop:42"5stale value served6
During a rollout the two application versions write to different cache namespaces, so an invalidation from the old version cannot reach the value the new version cached. Reconstructed from identity_cache issue #535.
Diagram source
05

Numbers you can plan against

Everything quantitative found in the corpus, with the artefact it came from and the date it was read. Where a figure has no date, the artefact did not carry one.

MetricValueAtContextAs ofSource
Platform boot time25s to 6.5sShopifyRoughly 75% faster after path pre-scanning and compile cachingread 2026-09bootsnap README
Boot time, second operator6s to 3sDiscourseRoughly 50%, reported by the operator in the same READMEread 2026-09bootsnap README
Boundary check, whole monolith3m30s to under 20sShopifyHot content-addressed cache; cold cache is unchanged2022-02packwerk PR #169
Build cache size~200 MBShopifyCompared in the PR with bootsnap at 99 MB and Sorbet at 148 MB2022-02packwerk PR #169
Capacity spent probing a dead resource16.7%ShopifyDefault MySQL settings, 1.0s half-open timeout against a 5.0s error timeout; 8.3% with bulkheads2019-07semian issue #244
Wasm module, dynamic linking1 to 16 KBJavyThe size class an extension platform can afford per requestread 2026-09javy README
Wasm module, static linkingat least 869 KBJavySame toolchain, interpreter embedded; two orders of magnitude largerread 2026-09javy README
JIT code memory ceiling64 MiBRuby 3.2Default --yjit-exec-mem-size; call threshold defaulted to 30Ruby 3.2Ruby 3.2 NEWS
Production exposure before "not experimental"over a yearRuby / Shopify"tested on production workloads for over a year and proven to be quite stable"Ruby 3.2Ruby 3.2 NEWS
Second JIT, relative speedslower than YJITRuby 4.0"faster than the interpreter, but not yet as fast as YJIT"; target is production readiness in 4.1Ruby 4.0Ruby 4.0 NEWS
Legacy storefront framework, last release2025-03-19ShopifyBranch v1.x-2022-07, years after the successor shipped2025-03hydrogen-v1 commits
Driver adoption to upstream11 daysRailsAdapter PR opened 2023-04-06, merged 2023-04-17, after the monolith had run it for weeks2023-04rails PR #47880

Three of these are worth planning against directly. The 16.7% figure is the only public measurement in the corpus of what circuit-breaker recovery costs, and it generalises: the tax is the half-open timeout divided by the sum of the half-open and error timeouts, multiplied by the fraction of workers allowed to probe. Set those three numbers deliberately and you have chosen your recovery budget; leave them at defaults and you have inherited someone else's. The Javy size figures are the admission rule for extension platforms, and the ratio between them, roughly fifty to one, is why dynamic linking of a shared engine is not an optimisation but a precondition. The boundary-check figures set expectations for anyone adding static enforcement to a large codebase: assume minutes before caching, assume tens of seconds after, and assume a cache measured in hundreds of megabytes on every developer machine and CI node.

Read these carefully

Every figure here is self-reported by the team that built the thing, in its own repository, and none has been independently reproduced. The boot-time and check-time numbers are the strongest, because they are reproducible by anyone with the code. The 16.7% is a calculation from stated defaults rather than a production measurement, and the issue presents it as such. No figure in this corpus describes request-level performance of the Shopify platform itself, because no such figure appears in a repository reachable from this session.

06

The evidence wall

Thirty-two cards here, thirty-six rows in the ledger that ships beside this page as sources.md, every one fetched on 11 September 2026 and every one from the same two hosts. Filter by kind.

What is missing, and why

This session's network policy reached github.com and raw.githubusercontent.com and nothing else. Shopify's engineering blog, railsatscale.com, Ruby's issue tracker, conference video and the MPLR 2023 YJIT paper were all tested and all blocked. There are therefore zero blog, talk and paper rows in this ledger, and that absence is a property of the session rather than of the topic. Two documents this guide can name but could not read are worth a reader's time on an unrestricted network: "A Packwerk Retrospective" by Gannon McGibbon and Chris Salzberg, named in the pull request below, and the MPLR 2023 YJIT evaluation cited in Ruby's own documentation.

Postmortem Shopify2019-07

semian #244: throttle half_open to closed attempts

An engineer quantifies how much worker capacity is consumed probing a dependency that is still down, at the library's own default timeouts, and proposes tying ticket counts to circuit state. Open since July 2019.

Carry forwardRecovery traffic is a capacity budget. Set the probe concurrency explicitly or inherit a 16.7% tax.
https://github.com/Shopify/semian/issues/244
Postmortem Shopify2023-04

identity_cache #535: invalidations missed on schema rollout

The maintainer describes how schema-versioned cache keys create a one-directional invalidation window during any rollout, how ordinary a trigger can be, and proposes moving the schema hash into the value with a UUID data version.

Carry forwardVersion in the key equals partition at deploy time. Put the version in the value.
https://github.com/Shopify/identity_cache/issues/535
Postmortem operator report2018-03

bootsnap #144: Rails boot failure when the cache dir is not writable

A read-only container image turns a boot accelerator into a boot blocker, with the exact errno in the report. The requested behaviour, warn and continue, is the general rule for every performance cache.

Carry forwardDeclare, and test, what your cache does when its store is unavailable.
https://github.com/rails/bootsnap/issues/144
Postmortem Shopify2026-01

ruvy, archived: Ruby does not fit in Shopify Functions

The repository states that its Wasm modules exceed the maximum size Shopify Functions accepts, lists ideas rather than a plan, and was archived read-only on 27 January 2026.

Carry forwardWrite the sandbox admission rule as a number first; then see which languages qualify.
https://github.com/Shopify/ruvy
Source Shopifysince 2014-10

semian: circuit breaker and bulkhead in the worker process

Failure containment for a monolith, running since October 2014, with adapters for MySQL2, Redis, Net::HTTP and both Active Record adapters. No sidecar, no extra hop.

Carry forwardWith many processes per host, host-local semaphores are a credible substitute for a mesh.
https://github.com/Shopify/semian
Source Shopifysince 2014-10

toxiproxy: fault injection in every development environment

Built because existing tools "didn't provide the kind of dynamic API we needed for integration and unit testing", and used in all development and test environments since the same month Semian went in.

Carry forwardShip the failure-injection tool to laptops, not only to game days.
https://github.com/Shopify/toxiproxy
Source Shopify2022-02

packwerk #169: caching the boundary check

An outside contributor takes the whole-monolith check from three and a half minutes to under twenty seconds with a content-addressed cache, and documents the cache size next to bootsnap's and Sorbet's.

Carry forwardStatic enforcement is a build-time cost; budget the cache before adopting it.
https://github.com/Shopify/packwerk/pull/169
Source Shopify2024-05

packwerk #389, closed unmerged: no link to our own retrospective

A contributor proposes linking Shopify's Packwerk retrospective from the README. A maintainer declines: "Those articles get old and change and might no reflect what the tooling is anymore."

Carry forwardThe rejection record tells you what a project thinks it is. Read the closed pull requests before adopting a tool.
https://github.com/Shopify/packwerk/pull/389
Source Shopify2022-10 to 2023-04

semian #420: the resiliency layer meets a new driver

Six months of work, closed unmerged, on classifying Trilogy's errors well enough for a circuit breaker: the driver "seems eager about closing unused connections" and the half-open timeout needed new client instances.

Carry forwardSwapping a driver means re-deriving your failure taxonomy. Cost that in, not just the wire protocol.
https://github.com/Shopify/semian/pull/420
Source Shopifysince 2017-05

job-iteration: jobs built for tenant moves

Interruptible, resumable background jobs, used "to interrupt workloads safely when moving tenants between shards and move shards between regions". The clearest public statement of how the platform is partitioned.

Carry forwardIf tenants move, every long-running job is a migration hazard until it can checkpoint.
https://github.com/Shopify/job-iteration
Source Shopifyread 2026-09

identity_cache README: the stated inconsistency budget

"IdentityCache is never going to be 100% consistent, since cache invalidations can be lost", followed by the list of ways they are lost, from skipped callbacks to Memcached restarts.

Carry forwardWrite your cache's failure modes into the README. Operators plan against stated budgets, not implied ones.
https://raw.githubusercontent.com/Shopify/identity_cache/main/README.md
Source Shopifyread 2026-09

packwerk: boundaries without services

Packages, public constants, and an epigraph that frames the whole strategy: "This knowledge is a dependency that raises the cost of change." The tool is still receiving pull requests in 2026.

Carry forwardDependency cost and deployment cost are separable. Only pay the second when you need it.
https://github.com/Shopify/packwerk
ADR Shopifyread 2026-09

packwerk USAGE.md: the todo file as a ratchet

package_todo.yml records existing violations to be "worked off over time"; enforce_dependencies: strict stops new ones being recorded at all. Two settings, two eras of a codebase.

Carry forwardRetrofit a checker with a recorded baseline, then ratchet. A blocking checker on day one is a disabled checker on day two.
https://raw.githubusercontent.com/Shopify/packwerk/main/USAGE.md
ADR Ruby / Shopifyread 2026-09

doc/jit/yjit.md: a JIT built inside the interpreter

"A lightweight, minimalistic Ruby JIT built inside CRuby" using basic block versioning, written in Rust, with production stories solicited to a Shopify address and an MPLR 2023 paper cited for the evaluation.

Carry forwardImproving a runtime in place beats replacing it when the improvement can be upstreamed rather than forked.
https://raw.githubusercontent.com/ruby/ruby/master/doc/jit/yjit.md
ADR Ruby / Shopifyread 2026-09

doc/jit/zjit.md: the second bet, method-based and profile-guided

"A method-based just-in-time (JIT) compiler for Ruby" that "uses profile information from the interpreter to guide optimization", described as an advanced prototype.

Carry forwardA substrate investment is a standing commitment. The second compiler is a new multi-year project, not a version bump.
https://raw.githubusercontent.com/ruby/ruby/master/doc/jit/zjit.md
Source RubyRuby 3.2

Ruby 3.2 NEWS: "YJIT is no longer experimental"

Promotion justified by production exposure: "tested on production workloads for over a year and proven to be quite stable", with a 64 MiB default code budget and a call threshold of 30.

Carry forwardIf you fund a substrate, you are also volunteering to be its first production workload.
https://raw.githubusercontent.com/ruby/ruby/ruby_3_2/NEWS.md
Source RubyRuby 4.0

Ruby 4.0 NEWS: ZJIT ships experimental and slower

"As of Ruby 4.0.0, ZJIT is faster than the interpreter, but not yet as fast as YJIT", with production readiness targeted for 4.1 and Rust 1.85 required to build it.

Carry forwardPlan substrate work in multi-year increments, and expect the replacement to be behind the incumbent for years.
https://raw.githubusercontent.com/ruby/ruby/ruby_4_0/NEWS.md
Source Shopifyread 2026-09

Shopify/yjit: the repository that made itself redundant

"YJIT has been merged upstream, and is now an official part of Ruby 3.1+", with bug reports redirected to Shopify's fork of CRuby. The strategy in one banner.

Carry forwardThe success condition for substrate work is that your repository stops being where the work happens.
https://github.com/Shopify/yjit
Source Shopifyread 2026-09

yjit-metrics: benchmarking the bet in public

Continuous benchmarking and statistics for the JIT, published at speed.ruby-lang.org. A public scoreboard is how an upstream investment stays accountable to the company paying for it.

Carry forwardFund the benchmark harness at the same time as the optimisation, not afterwards.
https://github.com/Shopify/yjit-metrics
ADR Rails / Shopify2023-04

rails #47880: upstreaming the Trilogy adapter

Shopify "adopted Trilogy successfully in our Rails monolith several weeks ago" and then proposed the adapter to Rails, deliberately copying code verbatim first and cleaning up later. Merged eleven days after opening.

Carry forwardAdopt, prove in production, then upstream. A private adapter is a tax you pay every framework release.
https://github.com/rails/rails/pull/47880
Source GitHubread 2026-09

trilogy: the driver with a second operator

"Designed for performance, flexibility, and ease of embedding. It's currently in production use on github.com", with no dependencies beyond POSIX, libc and OpenSSL.

Carry forwardPrefer a component that a second organisation already runs in production. One operator is a dependency; two is a standard.
https://github.com/trilogy-libraries/trilogy
Case study Rails / Shopifyread 2026-09

bootsnap: the largest published figure, in someone else's organisation

"The core Shopify platform boots about 75% faster, dropping from around 25s to 6.5s", alongside Discourse's independent 50%. The gem now lives under rails/.

Carry forwardDonate infrastructure once it stops differentiating you; the maintenance is worth more than the logo.
https://github.com/rails/bootsnap
Source Bytecode Allianceread 2026-09

javy: the size rule that governs the extension platform

Modules of "1 to 16 KB range with use of dynamic linking" against "at least 869 KB" statically linked. The same toolchain, two orders of magnitude apart.

Carry forwardOn a per-request sandbox, shared-engine dynamic linking is a precondition rather than an optimisation.
https://github.com/bytecodealliance/javy
Source Shopifyread 2026-09

krane: a verdict layer over kubectl

Built because kubectl "leaves its users with some burning questions: What just happened? Did it work?" Renamed from kubernetes-deploy at 1.0, and still tracking current Kubernetes releases.

Carry forwardWhere you are a tenant of a platform, add the missing verdict rather than the missing platform.
https://github.com/Shopify/krane
Source Shopifyread 2026-09

shipit-engine: one deploy tool across many stacks

"Used Shipit to synchronize and deploy hundreds of projects across dozens of teams, using Python, Rails, RubyGems, Java, and Go", and it works with anything deployable by a script.

Carry forwardA script-shaped deploy interface outlives the platforms underneath it. Couple to the verb, not the runtime.
https://github.com/Shopify/shipit-engine
Source Shopifyread 2026-09

hydrogen: defined by its relationship to a router it does not own

"Hydrogen is designed to dovetail with React Router", and "Hydrogen legacy v1 has been moved to a separate repo". Two sentences that describe a whole storefront strategy.

Carry forwardWhen your framework's identity is an adjective attached to someone else's, expect to move when they do.
https://github.com/Shopify/hydrogen
Source Shopify2025-03

hydrogen-v1: the legacy that kept shipping

Behind a legacy banner, yet still cutting releases on branch v1.x-2022-07 as late as 19 March 2025, years after the successor.

Carry forwardDeprecation is a multi-year support obligation. Budget the old version's releases, not just the new version's launch.
https://github.com/Shopify/hydrogen-v1/commits/main
ADR Remix / Shopify2022-07-29

react-router 0005: Remixing React Router

The decision, accepted 29 July 2022, to move Remix's loaders, actions and fetchers down into React Router, with router logic extracted into a zero-dependency package and hooks renamed along the way.

Carry forwardWhen a framework pushes its ideas into its dependency, its users inherit the dependency's release cycle.
https://github.com/remix-run/react-router/blob/main/decisions/0005-remixing-react-router.md
ADR Remix / Shopify2024-09-18

react-router 0011: routes.ts, and an adapter for the people left behind

Acknowledges that "when Remix changed its routing conventions between v1 and v2, some users experienced friction", and ships a dedicated adapter package to carry Remix route config into React Router v7.

Carry forwardAn adapter package in a decision record is a priced migration. Count them to measure a dependency's churn rate.
https://github.com/remix-run/react-router/blob/main/decisions/0011-routes-ts.md
Source community2024-12-14

remix #10333: the migration tax, in users' words

"So you're asking Remix adopters to migrate Remix to React Router to Remix at some future date?", and from another commenter, "started three remix projects over the past 12 months and the setup was totally different every time".

Carry forwardRead a dependency's discussion tab before adopting it. Churn shows up there a year before it reaches your backlog.
https://github.com/remix-run/remix/discussions/10333
Source Remix / Shopifyread 2026-09

remix: the third foundation, away from React

"Welcome to Remix 3! The fully-stacked web framework", under active development, with stated principles including "Model-First Development (optimizing for LLMs)" and "Minimizing dependencies".

Carry forwardA framework that keeps changing its own foundation is telling you its roadmap. Believe it.
https://github.com/remix-run/remix
Source Shopifyread 2026-09

Shopify/ruby: the company keeps a checkout of its own language

A fork of ruby/ruby carrying more than 100,000 commits, used as the intake for JIT bug reports. Owning the constraint starts with being able to build it.

Carry forwardThe cheapest test of leverage over a dependency: can your team build and patch it today, from a checkout you maintain?
https://github.com/Shopify/ruby
07

Build a miniature, then productionise it

Six rungs. The first three can be done against a toy application; the last three only mean something against a codebase somebody else also works in.

Score your leverage over every runtime dependency

List the five substrates you could not replace in a quarter: language runtime, web framework, database driver, orchestrator, front-end framework. For each, answer three questions with evidence rather than opinion. Can we build it from source today? Has anyone here landed a change upstream? Do we know who decides its roadmap?

Done when: every row has a yes or no with a link, and at least one row surprises someone in the room.  Teaches: leverage is measurable, and it is usually lower than assumed.

Put a bulkhead and a breaker in front of your slowest dependency

Wrap one dependency in a circuit breaker with a host-local concurrency limit. Then use a fault-injection proxy to hold it down and watch the recovery path specifically: how many workers probe, for how long, and what fraction of capacity that is.

Done when: you can state your recovery tax as a percentage, the way semian #244 does.  Teaches: the breaker's defaults are a capacity decision somebody else made for you.

Make one long job interruptible, then kill it

Take the slowest batch job you own and give it a checkpoint per record. Deploy over it, evict the pod mid-run, and prove it resumes without reprocessing. Then answer the harder question: could you move the tenant this job is working on to another shard right now?

Done when: a mid-run kill leaves no duplicates and no lost work, verified by counting rows.  Teaches: data mobility is a property of your job framework before it is a property of your database.

Add a boundary checker with a recorded baseline

Introduce static package boundaries into a real codebase. Record every existing violation into a todo file, fail the build only on new ones, and measure the check on a cold and a hot cache. Publish both timings to the team before turning it on in CI.

Done when: the check runs in CI, the todo file is shrinking month over month, and nobody has asked to disable it.  Teaches: modularisation is a ratchet with a build-time price tag, not a migration.

Run an untrusted extension, and try to fit your own language in it

Compile a small function to WebAssembly and call it on a request path. Measure module size and cold start. Then compile the same logic from the language your platform is written in, and compare. Write down the size limit you would enforce and who it excludes.

Done when: you have two module sizes, two cold-start numbers, and a written admission rule.  Teaches: the sandbox is a separate platform with its own economics, which is exactly what Ruvy ran into.

Carry a patch to your runtime for a month

Fork the runtime you scored highest on in rung one. Apply a small change you actually want, build it, run your test suite against it, and rebase weekly for four weeks. Track the hours. That number is the true cost of the "we could always fork it" answer that every architecture review accepts without evidence.

Done when: you can state the monthly cost of carrying one patch, and decide on that basis whether upstreaming is cheaper.  Teaches: forking is a subscription, and upstreaming is how you cancel it.

08

Keep hunting

The queries that produced this page, against a network that reached only one host. They work on any company with a public repository presence.

Abandonment and reversal

  • site:github.com <org> archived "no longer maintained"
  • site:github.com <org> "has been moved to a separate repo" legacy
  • site:github.com <org> "is not currently compatible with"

The argument, not the conclusion

  • github.com/<org>/<repo>/pulls?q=is:pr+is:closed+is:unmerged
  • site:github.com <org> decisions "0001" OR "Status: Accepted"
  • site:github.com <repo> discussions "please reconsider"

Production numbers hiding in repositories

  • site:github.com <org> issue "in production since"
  • site:github.com <org> pull request "reduced from" seconds minutes
  • site:github.com <org> "% of capacity" OR "of our capacity" issue

Failure modes the vendor documented itself

  • raw.githubusercontent.com <org>/<repo>/main/README.md "never going to be"
  • site:github.com <org> issue "can be missed" OR "can be lost" invalidation
  • site:github.com <repo> issue "in production" errno OR "failed to boot"

One technique did most of the work here and is worth stating on its own. When a blog host is unreachable, the same facts are often inside a repository, written for a different purpose: a README that dates a system to a month, a pull-request description that carries a before-and-after timing, an issue that quantifies a failure mode because the author needed to justify the fix. Those sentences were written to persuade a colleague, which makes them better evidence than the post that was written to be found.

09

References

  1. Shopify, semian GitHub repository. Checked 2026-09-11.
  2. Shopify, semian issue #244, Feature Request: Throttle half_open to closed attempts GitHub, 2 July 2019. Checked 2026-09-11.
  3. Shopify, semian PR #247, Throttle bulkhead from the circuit breaker on state transition GitHub, 4 July 2019. Checked 2026-09-11.
  4. Shopify, semian PR #420, Adapter for GitHub's Trilogy MySQL client GitHub, opened 3 October 2022, closed 24 April 2023. Checked 2026-09-11.
  5. Shopify, toxiproxy GitHub repository. Checked 2026-09-11.
  6. Rails, bootsnap GitHub repository. Checked 2026-09-11.
  7. bootsnap issue #144, Rails boot failure when cache dir isn't writable GitHub, 22 March 2018. Checked 2026-09-11.
  8. Shopify, packwerk GitHub repository. Checked 2026-09-11.
  9. Shopify, packwerk USAGE.md GitHub. Checked 2026-09-11.
  10. Shopify, packwerk PR #169, Add caching to speed up bin/packwerk check GitHub, opened 21 December 2021, merged 16 February 2022. Checked 2026-09-11.
  11. Shopify, packwerk PR #389, closed unmerged GitHub, opened 22 February 2024, closed 2 May 2024. Checked 2026-09-11.
  12. Shopify, packwerk closed-unmerged pull requests GitHub. Checked 2026-09-11.
  13. Shopify, job-iteration GitHub repository. Checked 2026-09-11.
  14. Shopify, identity_cache README GitHub. Checked 2026-09-11.
  15. Shopify, identity_cache issue #535, Cache invalidations can be missed on rollout of schema changes GitHub, 5 April 2023. Checked 2026-09-11.
  16. Ruby, doc/jit/yjit.md GitHub. Checked 2026-09-11.
  17. Ruby, doc/jit/zjit.md GitHub. Checked 2026-09-11.
  18. Ruby, NEWS for Ruby 3.2 GitHub. Checked 2026-09-11.
  19. Ruby, NEWS for Ruby 4.0 GitHub. Checked 2026-09-11.
  20. Shopify, yjit GitHub repository. Checked 2026-09-11.
  21. Shopify, yjit-metrics GitHub repository. Checked 2026-09-11.
  22. Shopify, fork of ruby/ruby GitHub repository. Checked 2026-09-11.
  23. GitHub, trilogy GitHub repository. Checked 2026-09-11.
  24. Rails, PR #47880, Introduce adapter for Trilogy GitHub, opened 6 April 2023, merged 17 April 2023. Checked 2026-09-11.
  25. Shopify, krane GitHub repository. Checked 2026-09-11.
  26. Shopify, krane CHANGELOG GitHub. Checked 2026-09-11.
  27. Shopify, shipit-engine GitHub repository. Checked 2026-09-11.
  28. Bytecode Alliance, javy GitHub repository. Checked 2026-09-11.
  29. Shopify, ruvy GitHub repository, archived 27 January 2026. Checked 2026-09-11.
  30. Shopify, hydrogen GitHub repository. Checked 2026-09-11.
  31. Shopify, hydrogen-v1 GitHub repository. Checked 2026-09-11.
  32. Shopify, hydrogen-v1 commit history GitHub. Checked 2026-09-11.
  33. React Router, decision 0005, Remixing React Router GitHub, 29 July 2022. Checked 2026-09-11.
  34. React Router, decision 0011, routes.ts GitHub, 18 September 2024. Checked 2026-09-11.
  35. Remix, discussion #10333 GitHub, 14 December 2024. Checked 2026-09-11.
  36. Remix, repository GitHub. Checked 2026-09-11.