← All posts

Membership is a query, not a list

August 5, 2026 · Pushpal

Every fleet operator has met the group nobody dares delete. It is called pilot-devices or ring-1 or WSUS-Group-Old-DO-NOT-USE, it was assembled by hand in 2019, half its members were decommissioned years ago, three of them are somehow production database hosts, and the person who knew why is gone. It still gets every early build, because removing a machine from a list feels like an action someone might have to defend, and leaving it feels free.

That group is not an accident. It is the natural end state of a design decision the industry spent decades unwinding: storing membership as a list.

The era of rosters

The first generation of software distribution tooling managed groups the way a spreadsheet would. Active Directory organizational units and security groups decided which machines got which policies. SCCM and WSUS collections decided who got patches, and a collection was, under the hood, a set of machine names somebody maintained. Early MDM products copied the pattern: create a device group, drag devices into it, assign the update to the group.

Rosters have one virtue: you can read them. And they have one flaw that outweighs it: a roster describes what someone once said about a machine, not what the machine is. The moment a device is reimaged, resold, repurposed or retired, the list is wrong, and nothing forces anyone to notice. Drift is not a failure mode of stored membership. It is the default behavior.

The operational symptoms became folklore. Staged rollout "rings" that had quietly stopped resembling the fleet. Pilot groups full of the machines least like production, because those were the ones nobody minded risking. Compliance reports that were true of the list and false of the world.

The era of attributes

The unwinding started, as it often does, with infrastructure becoming disposable. When AWS shipped resource tagging in 2010, the interesting part was not the tags. It was what tooling built on top of them: Systems Manager and autoscaling groups let you say "run this on every instance tagged env=staging", and the set of matching instances was computed when the command ran. Nobody maintained the list. There was no list. Machines that launched an hour ago were members the moment they matched.

Directory services followed. Azure AD dynamic groups replaced dragged-in members with a rule: device.os -eq "Windows" and device.version -startsWith "10.". Membership became a standing query the directory evaluated continuously.

Kubernetes made the idea a first-class citizen and gave it the vocabulary most engineers now use. Labels on objects; selectors on everything that needs to choose objects. A Service does not hold a list of pods, it holds matchLabels, and the set of pods behind it is recomputed as pods come and go. And when exact-match proved too coarse, Kubernetes added matchExpressions: operators like In, NotIn, Exists, Gt and Lt, so a selector could express "this key, but not these values" or "any node above this generation". That two-part shape, exact matches plus operator expressions, ANDed together, turned out to be the sweet spot: expressive enough for real policies, simple enough to read in a code review.

The conclusion the industry converged on, across three very different domains, is compact enough to state in one line: membership should be computed at decision time from what a machine is, not stored as what someone once said it was.

Three properties fall out of it. There is no drift, because there is no stored state to go stale. There is no cleanup, because there is nothing to clean. And the thing you audit is the rule, which is one reviewable line, instead of the roster, which is ten thousand unreviewable ones.

Update fleets got this last

Web infrastructure got selectors a decade ago. The updater running on a hundred thousand customer desktops mostly did not. Desktop and device update pipelines still commonly stage rollouts with hand-picked device lists, "beta" flags baked into builds, or - the quiet failure - separate binaries per ring, which turns a targeting problem into a build-matrix problem.

The irony is that update checks are the perfect place for computed membership, because the device tells you what it is on every single check-in. An update request already carries platform, architecture and current version. That is a live attribute set, refreshed at exactly the moment you need to make the decision.

This is how targeting works in Relayer. A release carries rules in the two-part shape the industry settled on:

  • matchLabels - exact-match label rules, AND across keys, any-of within a key's values. Labels are opaque key/values you attach (customer, site, tier); Relayer stores and matches them, never interprets them.
  • matchExpressions - decision-time predicates: { key, operator, values } with In, NotIn, Exists, DoesNotExist, and semver-aware VersionGte / VersionLt. Keys resolve against the device's labels, or against the request itself through the reserved fields $version, $platform and $arch.

That last part matters more than it looks. "Serve 2.1.0 only to macOS devices still below 2.0" is one rule on the release:

{
  "matchExpressions": [
    { "key": "$platform", "operator": "In", "values": ["darwin"] },
    { "key": "$version", "operator": "VersionLt", "values": ["2.0.0"] }
  ]
}

No client change, no label to distribute, no list to maintain. The membership of that cohort is computed on every check-in from facts the device was already sending.

Two rules that keep queries honest

Computed membership has sharp edges, and pretending otherwise is how tools lose trust. Two design decisions in Relayer exist specifically to blunt them.

Everything fails closed. If a rule's key cannot be resolved - the device never got the label, the field is empty - the device is excluded from that release, and falls through to the newest release it does qualify for. The dangerous direction for an updater is surprising inclusion, a build reaching machines it was never meant for. A missing label should never mean "eligible". The one deliberate exception is DoesNotExist, whose entire meaning is "the key is absent".

Expressions freeze at the first offer. Rollout percentages in Relayer only ratchet up once a release has been offered to a real device, and label rules may only broaden, because narrowing after the fact strands machines that were already told an update exists. For expressions we went stricter: once a release has been offered, its expressions cannot change at all. The honest reason is that "does this edit broaden or narrow?" is answerable for exact-match labels and not reliably answerable across interacting operators, and a wrong answer here silently strands devices. A frozen rule you must replace by cutting a new release is a better contract than a flexible rule that is sometimes wrong. Loosening this later is backward-compatible; the reverse would not have been.

And because a query is harder to eyeball than a roster, the query has to explain itself. Every release in Relayer has a device page listing each machine in the channel with the verdict the real decision function gives it - offered, outside the rollout bucket, excluded by which exact rule - and a preview that traces any hypothetical device through the same code path production uses. If membership is computed, the computation must be inspectable. That is the deal.

The list is a cache. Stop writing to it.

If there is one transferable lesson from thirty years of group management, it is that a stored membership list is a cache of a query someone ran in their head, and like every cache with no invalidation strategy, it is wrong from the moment it stops being looked at. The industry's answer - tags on AWS, dynamic groups in directories, selectors in Kubernetes - was to keep the query and throw away the cache.

Your update pipeline deserves the same. The devices are already telling you what they are, on every check-in. Ask them, every time.

Targeting is live in Relayer today - the full semantics are in the targeting guide, and the mental model behind the decision walk is in concepts. If you want to see a rule refuse to do something dangerous, try narrowing one after its release has shipped.