Routers, Buckets and Shopify Goes Native · Morning Gist
Thursday, October 85 stories · 16 min
Routers, Buckets and Shopify Goes Native
Today: how a sharded Postgres router and admission control work, mesh versus hub VPN design, Shopify leaving React Native, and Sam Newman's resilience heuristics.
Picked from 54 new articles across 34 sources · arrow keys for other days
Today in 30 seconds
30s left
PlanetScale rebuilt Postgres auth, the wire protocol and an 18,000-line parser in Go so its sharding router can pose as one server. story 1 ↓
A leaky bucket needs just two stored values. PlanetScale's Traffic Control uses one to reject Postgres queries before they overload the database. story 2 ↓
Tailscale argues router and NAS VPNs fail on patching and reliability. It's a vendor pitch, but its survey of vague patch promises holds up. story 3 ↓
Shopify is dropping React Native. It rebuilt the Shop app natively in 12 weeks, and the 300-plus-screen Shopify app is next. story 4 ↓
Sam Newman calls microservices a last resort, says most outages come from running out of resources, and explains the trade-offs of adding idempotency later. story 5 ↓
1/5PlanetScale· PlanetScale· Sep 10
How PlanetScale's sharded Postgres router plans and runs a cross-shard join
PlanetScale follows one join query through Neki, its sharded Postgres system: authentication, parsing, cost-based planning of a hash join in the router, pooled connections to the shards, and why co-locating related rows avoids the join entirely.
Why it matters · The walkthrough shows where a distributed SQL layer really spends its effort, and how the shard key you choose decides whether the work runs on the shards or in a router.
Know this first
Context from the editor, not the article · 35 sec
1 · What sharding means
Sharding splits one logical database across many servers. Each row's shard key (a chosen column) is hashed, and the hash decides which server stores the row. This lets the database grow past one machine, but a query that needs rows from several shards must collect them from several servers.
2 · Postgres connections are expensive
Postgres starts a separate operating-system process for every client connection, and that process uses memory even while idle. Large deployments therefore put connection poolers in front of Postgres, so many clients share a small set of reused connections.
3 · Join algorithms
2/5PlanetScale· Simeon Griggs· Oct 7
How leaky buckets decide whether work runs, and PlanetScale's version for Postgres
A PlanetScale post traces the leaky bucket from ancient water clocks to API rate limiting. It then describes Traffic Control, which uses token buckets to admit or block Postgres queries before they run, based on estimated cost.
Why it matters · Databases that can't reject work they can't finish tend to fail in uncontrolled ways. Admission control turns that overload into a failure you planned for and can handle.
Know this first
Context from the editor, not the article · 35 sec
1 · What admission control is
Admission control is any mechanism that decides, before work starts, whether to accept or reject it. Examples are rate limiters on APIs, connection caps on servers and queue limits. The alternative is accepting everything and letting some bottleneck fail on its own terms.
2 · Leaky versus token buckets
Both are rate-limiting algorithms. A leaky bucket fills as work arrives and drains at a fixed rate. Work is refused when it would overflow. A token bucket flips this: tokens refill at a fixed rate and work spends them. The two behave the same, and both allow short bursts up to the bucket's size.
3 · Postgres's existing limits
3/5Tailscale· Andrew Cunningham· Sep 11
Tailscale argues router and NAS VPN servers fail on reliability and security
In a Tailscale blog post, Andrew Cunningham argues that the VPN servers built into home routers and storage boxes are unreliable, slow, hard to administer and poorly patched. He pitches Tailscale's peer-to-peer mesh as the fix.
Why it matters · It is a short case study in hub-and-spoke versus mesh design: where the choke point sits, who keeps the access list, and whose patch schedule you depend on.
Know this first
Context from the editor, not the article · 39 sec
1 · Hub-and-spoke VPNs
A classic remote-access VPN runs one server, the hub, inside your network. Every remote device opens an encrypted tunnel to it, and all traffic passes through that box and its internet link. The server has to be reachable from outside, which usually means a public IP address and a forwarded port.
2 · NAT and why it blocks
NAT (network address translation) lets many devices share one public IP. Unsolicited inbound packets have nowhere to go, so devices behind NAT are hard to reach directly. Carrier-grade NAT, firewalls that block UDP, and captive Wi-Fi portals all break VPNs that expect a simple path to a fixed endpoint.
3 · WireGuard vs OpenVPN
4/5Shopify Engineering· Sep 10
Shopify drops React Native and is rebuilding its mobile apps natively
Shopify is replacing React Native, the cross-platform framework it adopted in 2020, with separate Swift and Kotlin apps. It argues coding agents have made maintaining two codebases cheap enough. Shop shipped first, after 12 weeks.
Why it matters · One of the best-known React Native adopters is reversing a well-documented architecture bet. It shows how a team re-examines a decision once one of its cost assumptions changes.
Know this first
Context from the editor, not the article · 37 sec
1 · React Native versus native
React Native is Meta's framework for writing iOS and Android apps once, in JavaScript and React. "Native" means writing each app separately in the platform's own language: Swift for iOS and Kotlin for Android. That gives direct access to platform features but means maintaining two codebases.
2 · Shopify's 2020 bet
In 2020 Shopify moved its mobile apps to React Native and wrote about it publicly. It became one of the framework's best-known corporate backers and published widely used libraries such as FlashList, which renders fast scrolling lists. As recently as January 2025 it said it would keep investing.
3 · Brownfield versus greenfield
5/5The Pragmatic Engineer· Gergely Orosz· Oct 7
Sam Newman on resilient distributed systems, idempotency and microservices
On The Pragmatic Engineer podcast, Sam Newman described his new book on resilient distributed systems. He covered three rules for distributed systems, two ways to make retries safe, and why microservices should be a last resort.
Why it matters · These are compact design heuristics from someone with long consulting experience. The idempotency trade-off in particular is a decision many teams get wrong when they try to add it to an existing system.
Know this first
Context from the editor, not the article · 33 sec
1 · Fallacies of distributed computing
A list from Sun Microsystems engineers in the 1990s of false assumptions developers make about networks: that the network is reliable, latency is zero, bandwidth is infinite, topology doesn't change, and so on. Newman's "three rules" condense the eight items.
2 · Why idempotency matters
Networks lose replies. A client that times out can't tell whether the server never got its request or did the work and the reply got lost, so it retries. If the operation isn't idempotent, meaning safe to repeat, that retry can create a double charge or a duplicate order.
A nested-loop join takes each row of one input and searches the other input for matches. A hash join builds an in-memory lookup table from one input, then checks each row of the other input against it. Which one is cheaper depends on how many rows each side has.
Image: PlanetScale
PlanetScale has traced one SQL query through 1, its sharded Postgres system. The tour shows what it takes for a router to make many Postgres servers look like one.
The setup
Two tables, customers and orders, are spread over four shards. Each is hashed on its own id. The query joins recent orders to customer names. Because orders are placed by order id, one customer's orders end up on every shard while the customer row sits on just one.
Becoming Postgres
The router speaks Postgres to applications. It implements 2 authentication in Go; SCRAM is a challenge-response login in which the password never crosses the network. It supports both of Postgres's query protocols. The simple one sends the full SQL in one message. The extended one splits a query into Parse, Bind, Describe, Execute and Sync, so a prepared statement can be reused. The router then turns the SQL into an 3 using an 18,000-line Go parser that accepts and rejects exactly what Postgres does. A cached plan would let it skip parsing and planning, but this query is a cache miss.
Planning the join
The planner reads the schema from an "authoritative shard" and the routing map from 4, a distributed key-value store. Both tables need 5 requests, sent to all four shards. The article's illustrative numbers are 100,000 customers and an estimated 300,000 matching orders. A nested-loop join would need thousands of shard requests, one scatter per customer. A hash join needs eight: four to load customers into an in-memory hash table and four to stream orders that look themselves up in it. The cost model counts requests, rows moved and memory, and picks the hash join. It builds the table from customers because that side is smaller. If the table outgrows memory, the router partitions both inputs by join key and spills them to disk.
Execution
Requests go to a 6 next to each Postgres instance. They travel as protobuf messages over pooled gRPC streams and carry the session's role and settings. The sidecar shares warm Postgres connections across routers, resets mismatched settings, and checks the connection's role so no query inherits another user's permissions. Rows come back in batches and are matched and streamed to the client as they arrive. The router copies already-encoded values straight into results instead of decoding and re-encoding them. Averages are rewritten as per-shard sums and counts, then combined in the router.
The punchline
Shard orders by customer_id with the same hash, and each customer's orders sit beside the customer. The join then runs intact inside each Postgres instance: four requests, no hash table in the router. The article includes no latency figures and doesn't cover cross-shard writes or transactions.
PlanetScale has published a step-by-step walkthrough of one SQL query as it passes through 1, its sharded Postgres system. The goal is to show how much machinery it takes to make thousands of Postgres servers look like one. Neki's router, the process applications connect to, pretends to be Postgres. To do that it has to reproduce Postgres's authentication, wire protocol and parser, plan the query across shards, and share connections to the backends.
The setup
The example schema has two tables, customers and orders, spread over four shards. Each table is sharded by a hash of its own id. The query asks for recent orders along with the customer's name:
SELECT customers.name, orders.total, orders.created_at
FROM customers JOIN orders ON orders.customer_id = customers.id
WHERE orders.created_at >= $1;
The article warns early that this layout is naive. A customer's row lives on one shard, but that customer's orders are scattered across all four, because each order is placed by its own id.
Pretending to be Postgres
The router implements the full Postgres authentication exchange in Go, including 2. SCRAM is a challenge-response scheme: the client proves it knows the password by answering a challenge, and the password itself never crosses the network. The router then sends ReadyForQuery, the protocol message that tells the client it may send a query.
Postgres has two query protocols, and drivers differ in which one they use, so the router supports both. The simple protocol sends one Query message containing the full SQL with its values filled in. It can also carry several statements at once. The 3 splits the work into five messages:
Parse prepares the SQL with the $1 placeholder still in it.
Bind supplies the value and creates a 4, which tracks one execution of the statement.
Keeping preparation separate from execution lets an application reuse a statement without re-parsing it.
The router then parses the SQL into an 5, a structured representation of the query. Its Go parser is about 18,000 lines. It must accept and reject exactly the syntax Postgres does, and PlanetScale says it was tested to be at least as fast as Postgres's C parser. Before parsing, the router checks a cache of query plans. A hit skips both parsing and planning. Here the cache misses.
Planning across shards
To plan, the router needs two things. The first is the schema, which it caches from an "authoritative shard" that holds schema metadata. The second is the data topology, a JSON routing map stored in 6 (a distributed key-value store) and cached on each router. In the example topology, each table is hashed with xxhash and the hash space is split into four ranges: below 40, 40–80, 80–c0, and above c0.
Semantic analysis then resolves column names and types. The planner first builds a rough plan: a JoinCluster node holding the two tables and their join condition, plus an OutputClauses node listing the three columns to return. It then turns each table's share of the work into a Route node. A Route is a SQL query sent to one shard or to many. Since neither table can be narrowed by shard key here, both Routes are 7: they go to all four shards and the router collects the results.
The planner weighs several join strategies, including nested loop, hash join, merge join and batched nested loop, and it also considers swapping which table is read first. The article gives illustrative numbers. Suppose there are 100,000 customers and 1,000,000 orders. The planner's default estimate for a date inequality is that about a third of rows match, so it expects roughly 300,000 orders.
Nested loop: read every customer, then for each one run a scatter query for its orders. That moves about 400,000 rows, but over thousands of separate shard requests. Even a customer with no orders costs four queries.
Hash join: four shard queries fetch customers into an in-memory hash table keyed by id, then four more fetch the matching orders and look each one up. That is eight requests and still about 400,000 rows, including customers who never match.
The cost model weighs shard requests, rows transferred, rows processed and rows held in memory. It also tracks distinct-value counts on indexed columns to estimate how many rows a filter or join will match. With these numbers it picks the hash join. It builds the table from customers because 100,000 rows need less memory than 300,000. If the table outgrows its memory budget, the router spills to disk: it partitions both inputs by join key and joins matching partitions one pair at a time.
Execution and connection pooling
Each shard receives two plain queries. First, SELECT id, name FROM customers. Once the hash table is built, it receives the orders query, filtered by date.
The router does not connect to Postgres directly. It sends the work to a 8, a process running next to each Postgres instance. The request is a protobuf ExecuteRequest containing raw Parse/Bind/Execute/Sync bytes plus the session's database, role and settings. It travels over a pool of long-lived, two-way gRPC streams.
Because the session belongs to the router, not to any Postgres connection, every request carries its own session state. The sidecar keeps a shared pool of open Postgres connections that all routers draw from, which limits how many Postgres processes pile up. When it lends out a connection, it looks for one whose settings already match. If none does, it resets the old settings and applies the new ones. It also checks the connection's role, so a query can never run with the permissions of whoever used the connection before ("role poisoning").
Postgres then makes its own plan for the shard-local query. Rows flow back in batches, so the router can build the hash table while shards are still sending. When orders arrive, they are matched and streamed to the client before every shard has finished. One optimization: the router asks the shards to return values in the same text or binary format the client requested. It can then copy the encoded bytes straight into result rows instead of decoding them into Go values and encoding them again.
Some queries need real computation in the router. For AVG(orders.total) per customer, averaging each shard's average would be wrong, because one shard might hold two of a customer's orders and another twenty. Instead the router rewrites the average into per-shard sums and counts. Its evaluation engine adds those up and divides.
The real lesson: co-location
The article ends by pointing out that most of this work was self-inflicted. If orders is sharded by customer_id, using the same hash and ranges as customers, each customer's orders sit on the same shard as the customer. The whole join can then go to each shard intact, and Postgres runs it locally. That means four requests instead of eight and no hash table in the router. "The fastest router join is the one we don't have to run."
What's left open
This is a narrative tour, not a benchmark. The article gives no latency figures, router memory budgets or measured costs of spilling to disk. It defers the router's extended-protocol optimizations to a later post. It also says nothing about cross-shard writes, transactions or consistency. Those are the harder distributed-systems questions for a system that presents itself as a single Postgres server.
Remember
Shard related tables on the same key (orders by customer_id) so joins run locally on each shard rather than in a router.
Cross-shard aggregates need rewriting: an average becomes per-shard sums and counts, combined centrally.
A shared sidecar pool must reset session settings and verify the role, or reused connections leak state and permissions.
Stock Postgres can cancel a query that has already run too long (statement_timeout) and cap connections (max_connections). It has nothing that rejects an expensive query before it executes. Oracle and SQL Server have offered resource governors for years.
Traffic Control's per-query limit setting, which caps how much any single query may consume.1 / 2
A PlanetScale post by Simeon Griggs explains the 1, the oldest trick in rate limiting. It then describes Database Traffic Control, the company's 2 for Postgres. Admission control means deciding whether work may run before it starts. The post is partly a product pitch and contains no benchmarks, but the mechanism is clearly explained.
An old idea
Vessels that drained through a hole served as clocks from around 1600 BC. Athenian courts timed speeches with them and gave more water to more serious cases, an early form of admission control. In networking, Jonathan Turner is credited with coining "leaky bucket" in 1986 for policing packet rates.
How it works
Each piece of work has a cost. If adding that cost would overflow the bucket, the work is rejected. Otherwise it is admitted and the level rises. The bucket drains at a fixed rate based on elapsed time, so bursts are allowed up to the bucket's capacity while the long-run rate stays capped.
It is cheap because nothing actually drains in the background. The bucket stores two values, its last level and when that was measured, and computes the current level when asked:
Shopify's 3 Admin API uses this per app and per store. It charges each query's estimated cost before running it, refunds or charges the difference afterwards, and reports the bucket's state so clients can slow down before they are throttled.
Why Postgres needs it
Griggs argues that a common way databases fail is that they can't refuse work they won't be able to finish. Some connection or hardware limit ends up rejecting work in an unplanned way instead. Oracle (1999) and SQL Server (2008) have resource governors, but Postgres only has after-the-fact limits such as statement_timeout.
Traffic Control
Traffic Control applies resource budgets to queries matched by tags, which come from SQL comments or from the sending user or application. Each budget includes:
a 4 (the same idea with tokens to spend instead of a level to fill). It refills at a "server share", a percentage of server capacity, up to a burst limit measured in 5. Cost is measured in capacity, not time, because one second on eight cores is not one second on one core;
a per-query limit on what a single query may use;
a maximum concurrent workers cap.
Query cost is estimated from the plan and from past queries of the same shape, then corrected after execution. A query must pass every budget it matches. Budgets can warn or block, and PlanetScale recommends starting in warning mode.
The post doesn't say how accurate the estimates are or how much latency the check adds.
PlanetScale, a hosted database company, has published an explainer by Simeon Griggs on 1, the old idea behind most software rate limiters. The post also promotes the company's Database Traffic Control, which applies that idea to Postgres queries. It is half history lesson and half product pitch, and it includes no performance measurements. The mechanism is still worth understanding, both on its own terms and as one way to give a database the ability to say no.
The question behind 2
Griggs starts with "should this function execute?" Some versions of that question are easy, such as whether the user is logged in or has permission. Harder versions ask whether there are enough resources right now, or whether this work matters more than everything else waiting. Mechanisms that answer the harder versions before work starts are called admission control. The leaky bucket is the most familiar one.
The idea is older than computers. From around 1600 BC, Babylonians, Egyptians and Persians measured time with vessels that drained through a small hole. By about 200 BC, Greek engineers used a float to regulate the flow into a second vessel that drove a pointer, and Ctesibius added gears to advance the display. Athenian courts used water clocks as literal admission control: a speaker could talk until the vessel drained, and more serious cases got more water. In computing, Jonathan Turner is credited with first using the term "leaky bucket", in a 1986 IEEE Communications Magazine article about policing how fast packets enter a broadband network.
How a leaky bucket works
Each unit of work gets a cost. The bucket has a capacity and a current level. When work arrives:
if level + cost would exceed capacity, the work is rejected;
otherwise it is admitted and its cost is added to the level.
The bucket drains at a fixed rate based on elapsed time, not on when the work finishes. Over any long window, admitted work therefore can't exceed the drain rate. In the short term, work can burst up to the full capacity.
Griggs separates two uses. As a meter, the bucket decides whether work proceeds immediately. As a queue, it holds work that waits and starts only as the bucket drains. The post covers the meter.
Why it's cheap
No background process is needed to do the draining. A bucket stores only two values: its level the last time anyone checked, and when that was. The current level is computed on demand:
Here is an illustrative example. A bucket has capacity 10 and drains 2 units per second. At t=0 its level is 6. A request costing 5 arrives at t=1.5. The bucket has drained 3 units in the meantime, so the level is 3. Adding 5 gives 8, which fits, so the request is admitted and the bucket stores (8, t=1.5). If a second request costing 4 arrives right after, 8 + 4 = 12 exceeds 10, so it is rejected. Every check does constant work, and a bucket that has fully drained can simply be thrown away, which matters when you keep one bucket per user or per tenant.
A production example: Shopify
Shopify's 3 Admin API (GraphQL is a query language that lets clients request exactly the fields they need) gives each app on each store a bucket with a fixed size and restore rate. A request is charged by what it asks for, not by the number of requests. The bucket must have room for the query's estimated cost before it runs. Afterwards, the difference between estimated and actual cost is refunded or charged. Responses report the bucket's state, so clients can slow down before they are throttled. That makes throttling an expected outcome that clients plan for, not a surprise.
Why databases need this
Griggs argues that one of the most common production failure modes for databases is that they cannot deliberately reject work they can't complete. Send a relational database far more queries than it can handle and it will try to run all of them. Eventually some limit in hardware, configuration or connections starts refusing work in an unplanned, unhandled way. That limit becomes the admission control by accident.
Proprietary databases have offered throttling for a long time. Oracle's Database Resource Manager (1999) maps sessions to groups by user, service or client and limits their resources and concurrency. SQL Server's 4 (2008) uses classifier functions to assign sessions to rule sets. Griggs notes these throttle work but don't reject it. Open-source Postgres and MySQL have no built-in equivalent. Postgres's statement_timeout kills a query only after it has already used resources.
Traffic Control's design
PlanetScale's Traffic Control lets you define resource budgets. Each budget matches queries by tags. A tag can come from a SQL comment in the query, or from the database user or application that sent it. A query that fails a budget is either logged as a warning or blocked, depending on the budget's mode. PlanetScale recommends starting in warning mode, watching how much traffic would be affected, updating the application to handle blocked queries, and only then switching to blocking.
A budget has three main settings:
Server share and burst limit, a 5. Server share is the refill rate, expressed as a percentage of the server's resources. Burst limit is the bucket's capacity, expressed in . A query's cost isn't known until it runs, so Traffic Control estimates it from the and from the history of queries with the same shape. After execution it refunds or charges the difference, as Shopify does.
A query can match several budgets and has to pass all of them.
Cost is measured in server capacity, not wall-clock time, because time is not a fair unit: one second on eight cores is not the same as one second on one core. A 25% share means a quarter of the server's capacity is restored every second, whether the machine has 2 cores or 64. As an illustrative reading, not one the post spells out: on a 16-core server, a query that keeps 8 cores busy for one second would cost about half a whole-server second.
What the post leaves open
The design depends heavily on how good the cost estimates are. Postgres planner estimates can be badly wrong, and Griggs himself names planner failures as a source of overload. The post doesn't say how far estimates diverge from actual costs, how the history of "query shapes" is kept, what latency the admission check adds, or how resources other than CPU, such as I/O and memory, enter the "server share" measure. It also has no results from real workloads. The post's lasting point, which applies beyond this product, is that an admission limit forces you to decide in advance what your application does when work is refused.
Remember
A leaky bucket needs only two stored values, last level and timestamp, and drains lazily when checked.
Charging estimated cost up front and correcting it after execution works when the true cost is only known later.
Measuring query cost in server capacity, not wall time, makes a budget fair across core counts and parallel queries.
Both are VPN protocols. OpenVPN is older, built on TLS (the protocol behind HTTPS), and usually runs in userspace, historically on a single thread. WireGuard is a small, modern protocol that often runs in the kernel and generally gets more throughput from the same CPU. Tailscale is built on WireGuard.
Netgear router's built-in OpenVPN server settings, typical of the self-hosted VPN setup the post criticizes.
The argument
On Tailscale's company blog, Andrew Cunningham argues that the VPN server built into a home or small-office router or 1 (network-attached storage, a small always-on file server) isn't worth using. Netgear, TP-Link, Asus, Synology and QNAP all offer one, and many support 2, a modern encrypted-tunnel protocol. Cunningham ran one on his own Netgear router. It worked, he says, "except for the times when it didn't." This is a vendor post, but its list of problems is specific.
Four complaints
Reliability. The box makes setup easier but does nothing to get a connection through. It has no answer to restrictive 3 (network address translation, which hides devices behind a shared public IP and drops unsolicited traffic). His tunnel often failed on airplane Wi-Fi and phone hotspots, and users who can't connect simply turn the VPN off. The device is also a single point of failure: a firmware reboot or a dead NAS drive takes the VPN down too.
Performance. Router and NAS processors have little spare capacity for encrypting traffic from many users at once. Every user's traffic also leaves through the site's upload link. His example is a 2 Gbps Comcast Business plan with only 300 Mbps upload. If the box only runs 4, an older TLS-based protocol, expect it to be slower than WireGuard.
Security. This is the strongest section. Synology, QNAP and Eero publish when security patches end. Ubiquiti and Asus keep end-of-life lists but give no timelines. Netgear and TP-Link stop support three years after end-of-sale. Netgear only says devices "may" get critical fixes, and TP-Link sells several hardware revisions under one model number. Often you can't tell whether your router still gets patches.
Administration. Each user needs a hand-distributed profile, access limits are per-profile IP lists, and none of it connects to your real accounts. Cunningham argues this leads to profiles nobody revokes and profiles shared between people.
The pitch
Tailscale builds a mesh on WireGuard. Devices sign in through an existing 5 provider. A hosted 6, which coordinates how devices find each other, helps them connect directly through NATs. Only traffic between your devices goes through the tunnel (a "7"). Access follows user accounts, and permissions are managed in one console. Optional send all of a device's traffic through another device, like a classic VPN. bring in devices that can't run the client, such as printers.
Caveats
The post has no measurements. Data on a home NAS is still limited by that site's upload speed, mesh or not. When direct connections fail, Tailscale falls back to slower relay servers. And the dependency moves to Tailscale's service and your identity provider rather than disappearing.
The claim
Andrew Cunningham, writing on Tailscale's company blog, argues that the VPN server built into your Wi-Fi router or 1 (network-attached storage, a small always-on box that serves shared files) is "more trouble than it's worth." Netgear, TP-Link and Asus routers offer the feature, as do Synology and QNAP NAS boxes. Many now support 2, a modern encrypted-tunnel protocol. Cunningham ran one on his Netgear router and says it "worked fine! Except for the times when it didn't."
This is vendor content, and it ends with a Tailscale sales pitch. The useful parts are a clear list of the limits of a single-box, hub-and-spoke VPN and a survey of how vaguely router makers commit to security updates. He groups his complaints under four headings.
1. Reliability: nothing helps the connection get through
Cunningham's point is that a router or NAS makes setup easier, since it installs the software and generates the keys for you, but does nothing to get a connection through. An 3 or WireGuard server on your router has no way around a restrictive 4 (network address translation, which lets many devices share one public IP and drops unsolicited inbound traffic). He reports that his tunnel often failed on airplane Wi-Fi and phone hotspots. Sometimes it appeared to connect but then struggled to load pages. When the VPN doesn't work, users switch it off, which defeats the purpose.
Some general background the article leaves out: with a self-hosted server, the hard part is usually the path to your home endpoint. The home side might sit behind carrier-grade NAT with no public IPv4 address. The remote network might block UDP, the transport WireGuard uses, or non-standard ports. Either way, a fixed server address has no fallback route.
His second reliability point is the single point of failure. If a firmware update reboots the router, or the NAS goes down to replace a failed drive, the VPN goes down with it.
2. Performance: three bottlenecks
CPU. Router and NAS processors are sized for routing packets or handling drives, with little to spare. One tunnel is cheap. Many people connecting at once means encryption work the chip can't keep up with, so pages and file transfers slow down.
Upload bandwidth. With a hub, every remote user's traffic leaves through the office or home uplink, so your upload speed is the cap. His example is a 2 Gbps Comcast Business cable plan with 300 Mbps upload. Fine for "a device or three," he says, but thin once it's shared. Fiber plans are often symmetric.
Protocol. If the box only supports OpenVPN, expect it to be slower than WireGuard. The article doesn't say why. The usual explanation is that OpenVPN is TLS-based and runs in userspace, historically on one thread, while WireGuard has a much smaller code path and often runs in the kernel.
3. Security: patch windows nobody promises
This is the most concrete section. WireGuard's encryption is fine, Cunningham says, but the device running it will have bugs, and you depend on the vendor to ship firmware fixes. He compares vendors:
Vendor
Patch commitment (per article)
Synology, QNAP, Amazon Eero
Published documentation stating when patches end
Ubiquiti
"Vintage" (still updated) and "legacy" (generally not) lists, with no timeline
Asus
End-of-life list, with no timeline
Netgear
3 years after end-of-sale, but only "may" get "critical" fixes "as determined by Netgear"
TP-Link
3 years after last sale, but several hardware revisions share one model number, so support status is hard to tell
His conclusion is that for consumer routers you often won't know whether or when your support window has closed. For the device that guards your network's front door, that is a real risk.
4. Administration: a separate identity system
Under WireGuard on a router, each user or device needs its own profile, which tells the client where the server is and lets the server recognize the client. Profiles are installed by hand or by scanning a QR code. Access limits are usually written as per-profile lists of allowed IP addresses, and you may have to open firewall ports. The result is a second identity system, cut off from your real user accounts. Cunningham argues that this overhead leads to bad habits: profiles nobody revokes when people leave, and one profile shared by several people.
Tailscale's alternative
Tailscale is a mesh VPN built on WireGuard. You install a client on each device and sign in through an existing 5 provider such as Google, Microsoft or Apple. The devices form a private network Tailscale calls a "tailnet." Tailscale's hosted 6, the coordination service that tells devices how to find each other, distributes keys and addresses. Data then flows directly between devices over peer-to-peer WireGuard tunnels, and NAT traversal techniques punch through NATs where a fixed-server VPN would fail.
The claimed benefits map onto the four complaints:
The free Personal plan covers six users and unlimited devices.
What the post glosses over
Some limits survive the switch, and the post doesn't mention them:
Upload limits still apply to data stored at home. If you're pulling files from a NAS at home, peer-to-peer doesn't help: the NAS's uplink is still the limit. Mesh only removes the bottleneck for traffic that used to pass through the hub on its way somewhere else.
Direct paths don't always work. Peer-to-peer depends on NAT traversal succeeding. From Tailscale's public documentation, not this article: when it fails, traffic falls back to Tailscale's relay servers (called DERP), which are slower.
The dependency moves rather than disappears. "No software to patch" means no server to patch. You still update clients, and you now rely on Tailscale's control plane and your identity provider.
No measurements. There are no benchmarks or failure rates. The connection failures are one person's anecdotes.
The article's core argument still holds. A VPN is security infrastructure. Running it on cheap hardware with an uncertain patch schedule and a separate set of credentials concentrates risk in the place you can least afford it.
Remember
A hub VPN's throughput is capped by the site's upload link: 300 Mbps on a 2 Gbps cable plan.
Many router makers never say when patches end. Netgear's "may" and TP-Link's reused model numbers leave owners guessing.
A mesh VPN removes the hub as a bottleneck, but data stored at home is still limited by home upload speed.
A brownfield migration replaces an existing app piece by piece while it keeps shipping. A greenfield migration rebuilds the app from scratch alongside the old one. Brownfield is usually safer for large apps. Greenfield gives a clean design but traditionally means a long freeze on new features.
Helix rebuilding a Shopify app screen in Swift and Kotlin, checkpoint by checkpoint.
The news
Shopify is moving its mobile apps off 1, Meta's framework for writing one JavaScript/React codebase that runs on both iOS and Android. It is rebuilding them natively, in 2 for iOS and 3 for Android. The Shop app has already shipped as a native app, after 12 weeks from proof of concept to release. The main Shopify app, with more than 300 screens plus widgets, an Apple Watch app and Siri Shortcuts, is due later this year. The rest will follow.
Why reverse a successful bet
Shopify adopted React Native in 2020 for three reasons: to stop building features twice, to let developers work across the whole stack, and to stop chasing feature parity between platforms. The post says it delivered all three, at the price of steady work on performance, framework upgrades and dependencies. It also says Shopify's React Native apps are fast.
What changed is the cost of the alternative. In prototypes, coding agents ported features from one platform to the other and helped engineers work outside their usual stack. Shared specs, tests and review checkpoints kept the two apps in step. Two codebases still cost more, the author concedes, but no longer enough to decide the question. Native's benefits remain: closer access to platform features and Apple's and Google's tooling, and fewer layers in between.
Greenfield, with gates
Last time, Shopify migrated piece by piece (4), because a full rewrite would have frozen feature work for years. This time it is rebuilding from scratch (5).
Asking a model to translate a whole app in one pass produced code nobody could maintain. So Shopify built 6, a system that splits each screen into small checkpoints. Each checkpoint must pass tests, a visual comparison with the running app, two adversarial code reviewers and a human sign-off before the next one starts. Review feedback is kept and reused.
Feedback speed as architecture
Agents change code in seconds but need minutes to check it on a mobile simulator, because they read the screen through screenshots or the 7. Shopify's fix is to separate business logic completely from the UI. That logic runs "headlessly" (with no UI) on a desktop and is driven through a command-line tool, so checks take milliseconds. A remote mode drives real simulators with direct commands when the UI matters.
Open-source fallout and open questions
React Native Skia will be forked by its creator, with Shopify sponsoring it through 2026. 8, at about 2 million downloads a week, is looking for a new steward. Restyle will be archived after 2026. Shopify has published no figures on quality or on the long-term cost of maintaining two codebases.
The reversal
Shopify is moving all of its mobile apps off 1, Meta's framework for writing one JavaScript/React codebase that runs on both iOS and Android. It is rebuilding them natively, in 2 for iOS and 3 for Android. The Shop app, which often tops the shopping category in the app stores, has already shipped as a fully native app. The main Shopify app, used by merchants to run their stores, is being rebuilt and is due later this year. The other apps, including Point of Sale and Inbox, will follow.
This undoes a decision Shopify made in 2020 and defended publicly as recently as January 2025. The author (the post doesn't name them, though they wrote the 2025 piece) says React Native "remains an excellent framework" and that Shopify's React Native apps are fast. The reason for leaving is not that the framework failed. The cost that justified it has shrunk.
Re-deriving a decision from its assumptions
In 2020 Shopify gave three reasons for adopting React Native:
stop building every feature twice;
let developers work across the whole stack, including people with no mobile background;
spend less time keeping the iOS and Android apps in step ("feature parity").
The post says React Native delivered all three. It also came with ongoing costs. Shopify spent significant effort on performance tuning, on improving foundational parts of React Native itself, and on keeping up with framework upgrades and third-party dependencies. Those costs were acceptable while the alternative was writing every feature twice.
The argument for reversing is about which side of that trade-off moved. Shopify says it has used language models for coding since 2021. By late 2025, its prototypes of core screens rebuilt in Swift and Kotlin showed agents could do three things. They could port a feature from one platform to the other using the existing version as a reference. They could help engineers work outside their usual stack. And they could keep the platforms in step through shared specifications, tests and review checkpoints. The author is explicit that two codebases still cost more than one. The claim is narrower: the extra cost is "no longer the deciding factor." The benefits of native haven't changed. Shopify's code sits closer to platform features and Apple's and Google's own tooling, with fewer framework and dependency layers in between.
That reasoning applies beyond mobile. A decision rests on cost assumptions. When one of them changes, the decision needs to be worked out again rather than defended.
Why 4 this time
When Shopify moved to React Native, it migrated its largest apps 5, swapping parts in while the app kept shipping. A full rewrite would have taken years and frozen feature work. This time it chose greenfield, rebuilding each app from scratch. It gave three reasons. Agents are good at building Swift and Kotlin features with the React Native code as a reference. A clean slate removes old constraints. And prototypes showed a rebuild would be "substantially faster" than before. Shop went from proof of concept to app-store release in 12 weeks. The Shopify app is much larger: more than 300 screens, plus home-screen and lock-screen widgets, an Apple Watch app with watch-face complications, and Siri Shortcuts.
6: a gated porting loop
Shopify says the naive approach fails. If you point a model at the React Native codebase and ask for the whole native app in one pass, even after it has written detailed specs and task files, you get a large amount of code that can't be maintained or shipped. So the team built a system called Helix. It assumes the first attempt will be wrong and won't let a flawed attempt move forward.
It works like this. A developer points Helix at one screen. Helix reads the React Native code and proposes an ordered list of "checkpoints," small slices of the work that a person can review in minutes. Each checkpoint must pass four gates before it is committed and the next one starts:
tests that prove its behavior;
a visual review against the running app;
two adversarial code reviewers;
a human sign-off.
Feedback from every review is stored and reused, so the loop needs less human help as the migration goes on. Think of a compiler pipeline with strict checks after each stage (an analogy, not the post's): no single stage has to be perfect, because nothing passes the gate until it is correct. The post says this lets Shopify rebuild apps "in a fraction of the time." It gives no figures for review effort, defect rates or how much of the work is automated.
The real bottleneck: feedback latency
The most transferable part of the post is about testing speed. Agents can change code in seconds but take minutes to check the result on a mobile simulator. They read the screen through the 7 (the structured description of on-screen elements that assistive technologies use) or through screenshots. Shopify built tooling to let agents reproduce, fix and verify bugs on their own, but describes it as slow and brittle. React Native's hot module reload, which swaps in changed code without restarting the app, helped only partly, because the simulator itself was the slow step.
The fix is architectural. Business logic is fully separated from the user interface and can run "headlessly" (with no UI at all) on a desktop machine. It is exposed through a command-line tool (CLI). Through the CLI, an agent or a human can inspect app state, move between sections and perform actions without rendering anything, in milliseconds instead of minutes. When the real UI does matter, the CLI has a remote mode that drives a simulator with direct commands instead of reading layouts. The post says this also makes end-to-end tests fast and lets agents work unsupervised for hours. It is a familiar idea: keep the core logic separate from the UI and testable on its own. What's new is the reason. A fast test loop now sets the pace of the whole team, not just how pleasant development feels.
Unwinding the open-source commitments
Shopify is also handing off some of the React Native libraries it maintains:
React Native Skia, a 2D graphics library. Shopify sponsors it through the end of 2026. Its creator, William Candillon, will fork it under a new name, and the original repository will then be archived.
8, a high-performance list component with about 2 million downloads a week. Shopify will keep fixing critical compatibility breaks while it talks with other companies about taking it over long-term.
Restyle, a styling library with a smaller user base. It will be maintained through 2026, then archived, and is open to anyone who wants to fork it.
What's open
Shopify says each rebuild must match or beat today's performance, stability, accessibility and quality. It plans to measure success by product velocity, app quality and how much work agents complete on their own. It has not published any of those numbers. It also hasn't said what it will cost to keep two codebases in step over years rather than during a one-off port. That is the cost React Native was adopted to avoid, and Shopify's case depends on it staying low. Deeper write-ups on Helix and the agent-addressable architecture are promised.
Remember
Shopify isn't saying React Native failed. Its case is that the cost of maintaining two codebases dropped below native's benefits.
Gating each small slice with tests, visual checks and reviews kept the automated port from producing unmaintainable code.
Separating business logic so it runs without a UI cut feedback time from minutes to milliseconds.
A field that studies how complex socio-technical systems cope with surprises. It grew out of safety research in aviation, medicine and the military. David Woods is one of its founders and is often cited in software reliability work.
Image: The Pragmatic Engineer
Sam Newman wrote Building Microservices. On Gergely Orosz's Pragmatic Engineer podcast he called microservices an architecture of "last resort," while promoting his new book, Building Resilient Distributed Systems. The episode summary gives heuristics, not data. (This write-up leaves out its AI segment.)
What a microservice is
Newman was among about ten people at a Lake District architecture symposium when the name "microservices" was suggested. 1 colleague James Lewis and Martin Fowler published the first definition in 2014. Newman's strict definition today is a service you can deploy independently, without deploying anything else. His looser one is a service split by business domain rather than technical layer. Independent deployment is what lets teams work on their own.
Three rules
Newman condenses the eight 2 (classic false assumptions about networks) into three rules:
Information takes time to travel.
The thing you want to talk to sometimes isn't there.
Resources are finite.
In his experience, most outages come from rule 3: exhausted pools of connections, memory, CPU or bandwidth.
Idempotency: two approaches
An 3 operation has the same effect whether it runs once or several times. Payments need this so that retries don't double-charge.
Idempotency keys: the client sends a unique ID, usually a 4, and the server runs each key once. The design is clean but hard to retrofit, because client and server both change.
Fingerprints: the server hashes the request fields that matter and treats a matching hash as a duplicate. Only the server changes, but genuine repeats can be rejected and some real duplicates can still get through.
Newman worked on a Danish payment system that rejected same-value purchases within five minutes. Two identical coffees bought minutes apart would fail.
Business context and resilience
Newman argues that whether a system fails open (carries on without a broken dependency) or fails closed (refuses to proceed) is a business decision. He also uses four outcomes from resilience researcher David Woods:
Robustness: coping with expected faults, though the mechanisms add complexity.
Rebound: how fast the system recovers.
Extensibility: stretching to handle surprises.
Adaptability: changing over time, including between incidents.
Advice
Design module boundaries carefully first, and treat production as the truth. His reading list:
Fundamentals of Software Architecture by Ford and Richards
Khononov's Balanced Coupling
Parnas's 1972 paper on decomposing systems into modules
Fairbanks's paper on an architecturally-evident coding style
The summary only names topics such as 5 and 6. The detail is in the episode and the book.
Sam Newman wrote Building Microservices, one of the most-read books on the subject. On Gergely Orosz's Pragmatic Engineer podcast he called microservices an architecture of "last resort." The episode was recorded to mark his new book, Building Resilient Distributed Systems, out in print on October 7. What follows is a summary of a long conversation, not a technical paper. The ideas come with little supporting data, so read them as heuristics from an experienced consultant. This write-up leaves out the episode's long AI segment.
Where "microservices" came from
Newman was in the room when the word was coined. In the early 2010s, his 1 colleague James Lewis kept running into companies building service-oriented architectures in which each service could be replaced quickly. (Thoughtworks is a software consultancy.) At an architecture symposium in England's Lake District, Lewis suggested the name "micro apps" to a group of about ten people, and someone else suggested "microservices." Lewis and Martin Fowler published the first definition article in March 2014, and Newman's book followed in 2015.
Newman gives two definitions today:
The clear one: independently deployable. If you can change and ship a service without deploying any other service, it counts as a microservice.
The softer one: split by business domain. Services are cut along business functions such as billing or shipping, not technical layers such as the UI, logic and database tiers.
He stresses independent deployment because it lets teams work on their own. The episode summary doesn't spell out why he thinks microservices should be a last resort. Taken with the rest of the conversation, the implication is that you pay distributed-systems costs you don't pay inside a single process.
Three rules instead of eight fallacies
Newman found the eight 2 hard to remember, so he reduced them to three rules:
Information takes time to travel. Nothing moves instantly from A to B. Speed-of-light latency is a hard floor.
Sometimes the thing you want to talk to isn't there. Nodes, services and networks go offline.
Resources are not infinite. CPU, memory, storage and bandwidth run out eventually.
He says most distributed-systems outages he has seen come from rule 3: a resource pool running out. Think of connection pools, thread pools, file descriptors or disk. The episode's timestamps also include a segment on 3, where many clients retry or reconnect at once and swamp a recovering service, but the summary gives no detail.
Idempotency: keys versus fingerprints
An operation is 4 if doing it several times has the same effect as doing it once. Newman's analogy is an elevator call button: pressing "down" five times doesn't send five elevators. Payments are the classic case, because a retried or refreshed payment page must not charge the customer twice.
He describes two approaches.
Idempotency keys. The client generates a unique identifier, usually a 5, and sends it with the request. The server records keys it has already processed and runs each operation only once per key. If the client retries with the same key, the server returns the original result. Newman says this design is clean but hard to retrofit, because both client and server have to change.
Fingerprints. The client sends nothing extra. The server hashes the fields of the request that matter (account, amount, merchant) and treats a repeated hash as a duplicate. Only the server changes, so it is easy to add later. The cost is accuracy in both directions:
False positives: a genuine second request can look like a duplicate and get rejected.
False negatives: a real duplicate can slip through if, for example, some field changes between attempts.
Newman's example is a Danish payment system he worked on. It rejected purchases of the same value within a five-minute window. As an illustration (not from the article): a customer who buys two identical coffees in separate transactions three minutes apart would have the second one declined. The design swaps customer friction for avoided double charges. Which side of that trade you choose depends on the business, not just the technology.
Business context decides fail-open versus fail-closed
That point comes up elsewhere in the conversation. When a dependency fails, a system can 6 and keep going without it, or fail closed and refuse to proceed. Newman argues the choice belongs to business context. A recommendations widget can probably fail open. A fraud check guarding payouts probably shouldn't. He also calls 7 essential, meaning the ability to see from telemetry what a system is actually doing. The summary doesn't go into his specifics.
Four outcomes from resilience engineering
Newman often uses four concepts from David Woods, a resilience-engineering researcher whose work began with military systems:
Robustness: the system keeps working as designed when expected faults happen, as when 8 replaces a failing pod. Newman's caveat is that robustness mechanisms add complexity, and complex systems have more ways to fail.
Rebound: how fast the system recovers after something goes wrong.
Extensibility: how far the system can stretch to handle surprises and needs nobody anticipated.
Adaptability: how the system changes over time to take on new functions. Newman notes that resilient systems keep adapting between incidents, not only during them.
In this framing, resilience is more than redundancy. Redundancy covers only the first item. The other three depend on people, processes and how changeable the architecture is.
Design advice and reading
Asked how to get better at system design, Newman said to think carefully about module boundaries and the connections between them first, and that "production is truth." His reading list:
Fundamentals of Software Architecture by Neal Ford and Mark Richards
Balanced Coupling by Vlad Khononov
David Parnas's 1972 paper "On the Criteria to Be Used in Decomposing Systems into Modules"
George Fairbanks's 2010 paper on an "architecturally-evident coding style," which argues that code should make the system's design visible
What's left open
The summary claims a lot and demonstrates little. Newman's view that resource exhaustion causes most outages is based on his own experience, with no data behind it. The fingerprinting trade-off is described but not measured. The detailed material on thundering herds, observability and the trade-offs between resilience mechanisms is presumably in the full episode and the book.
Remember
Newman's strict test for a microservice is whether you can deploy it without deploying anything else.
In Newman's experience, most distributed outages come from exhausted resource pools, not network partitions or latency.
Fingerprint-based deduplication is easy to retrofit but rejects some genuine requests. Idempotency keys are cleaner but need client changes.
Describe asks which columns will come back and in what formats.
Execute runs the portal.
Sync marks the end of the batch.
6
7
Per-query limit, a cap on how much any single query may consume. This is checked against the query alone, not against what's left in the bucket.
Maximum concurrent workers, a similar per-query cap measured in worker processes instead of execution time.
8
Subnet routers
No hub. There is no small router CPU to overload and no single uplink that all traffic shares. Speed depends on the two endpoints' connections and the distance between them.
Split tunnel by default. Only traffic between tailnet devices goes through the tunnel. Everything else uses the normal internet path.
Identity-based access. Access comes and goes with the user's SSO account. Permissions live in one console as "grants" and "tags," and can start from groups already defined in the identity provider.
Optional extras. An 7 makes any tailnet device act as a traditional full-traffic VPN server. Cunningham's example is a conference-room Apple TV. Tailscale also integrates with Mullvad, a commercial VPN service. A 8 brings devices that can't run the client, such as printers, onto the tailnet. MagicDNS gives devices readable names instead of IP addresses.