---
title: 'Roaming and Band Steering'
source: 'https://academia.sh/en/courses/wireless-and-security/roaming-and-band-steering'
course: 'Wireless Networks and Network Security'
language: en
updated: '2026-08-17T18:07:23+00:00'
license: 'CC BY-SA 4.0'
---

# Roaming and Band Steering

As the handover threshold drops from 30 to 14, false admit falls from 8 to 0 and false deny rises from 0 to 8; early handover and late handover are two ends of the same dial, and fixing one direction grows the other.

The previous lesson overlapped cells on purpose: so no gap would remain, neighboring cells'
edges were deliberately overlapped. The measurement swept the budget and the clients stood
still — none of the forty clients moved.

Overlap was built exactly for those who move. A client at the edge hears two points at once; as
it walks, one weakens and the other strengthens. Then the association decision is made not once
but repeatedly, and every time the same question is asked: should I let go of what I have? What
this lesson measures is the threshold of that question. Raising the threshold lets the client go
early; lowering it makes it stick to a weak point. Both are errors, and **they do not show up in
the same numbers.**

## Who Makes the Handover Decision

**Roaming** is a client moving its bond from one access point to another. The first thing that
has to be said is who holds the decision: the decision **belongs to the client**. The network
can eject a client from a point, tell it about its neighbors, even suggest that it go to a
specific point; but the client itself chooses which point to associate with.

This directly explains why the measurement is two-directional. The only thing in the network's
hands is **thresholds**: below which signal strength the client is told "I'm not holding you
anymore," what gets reported in the neighbor list. The threshold is what the network writes; the
decision is what the client makes.

```text
# taught handover sequence , fictional and not executed

1. point A -> client        neighbor report
                              A: channel 4 , measured signal 21
                              B: channel 1 , measured signal 29
                              C: channel 7 , measured signal 12

2. client                   decision
                              signal in hand 21 , threshold 22 -> let go

3. client -> point B        association request

4. point B -> client        association response
                              result: accept , granted tier 4

5. point A                  old bond dropped
                              <-- frame loss occurs between step 2 and step 3
```

The note under the fifth step is roaming's real cost. Between two associations, the client is
bound to no point at all; frames arriving in that interval drop. If the interval is short, upper
layers absorb it as a latency ripple; if it is long, the connection looks broken. There are other
things that have to be re-established during a handover, and one of them is the subject of the
next lesson.

The first step also has a cost, and it is less visible. If the client does not know its
neighbors, it has to find them itself; finding them means listening to other channels. While it
listens to a channel, it is not on its own channel — meaning it misses frames arriving at that
moment. **Scanning produces loss even when no handover happens.** The more often a client scans,
the fresher its knowledge of neighbors, and the more frames it misses.

A **neighbor report** exists exactly to ease this trade-off: the network hands the client the
list it should search. The client now listens to the three points reported instead of ten
channels. But who writes the report is the previous lesson's question — an independent access
point does not know its neighbors, and without a shared view there is no list either. This is
where roaming's quality connects back to where the decision is made.

## Early Handover and Late Handover

The measurement's fiction is a **corridor**: two access points stand 50 m apart, and every client
sits somewhere along this line. The client's distance to A is the first lesson's `distance`
field; its distance to B is `50 - distance`.

Intent, in its plainest form, is this: the client should belong to whichever point it is closer
to. The overlap from the previous lesson creates an ambiguity here — a client between 20 and
30 m is counted inside both cells. For the oracle to give a single answer, the nearest-point rule
is chosen; the corridor's midpoint, 25 m, is the boundary.

The mechanism, though, does not know what intent can know. The client does not measure its own
position; it only measures the signal strength of the point it currently holds, and lets go when
that strength drops below the threshold. Because signal strength depends on both distance **and**
interference, two clients at the same distance let go at different times.

Two errors follow from this:

- **Early handover** — the client lets go while still close to A. Intent would have held it back,
  the mechanism let it through: **false admit**.
- **Late handover** — the client sticks to A while already close to B. Intent would have let it
  through, the mechanism held it back: **false deny**.

## What Band Steering Does Not Know

**Band steering** is a second handover decision: should the client be placed on the wide band,
whose range is short but whose tier ceiling is high, or the narrow band, whose range is long but
whose ceiling is low?

The right answer depends on what the client would actually get on the wide band. The problem is:
**the client has not tried the wide band yet.** When the steering decision is made, only the
narrow-band reading is in hand, and the decision is made from that reading alone. The rule is a
**proxy measurement** — it does not measure what it wants to measure, it measures something else
correlated with it.

The measurement's assumptions:

- **WN22.** Forty clients; distance and interference come from the first lesson's fiction. The
  signal budget is fixed at 52 in the roaming measurement.
- **WN23.** The corridor fiction: two points stand 50 m apart; the client's distance to B is
  `50 - distance`. Intent is the nearest-point rule; the boundary is 25 m.
- **WN24.** The mechanism sees only the signal strength of the point it currently holds.
  Position, B's signal, and the client's direction of travel do not enter the measurement.
- **WN25.** The swept handover thresholds are 30, 26, 22, 18, and 14. Only the threshold changes
  at a time.
- **WN26.** In the ping-pong measurement, the signal is assumed to swing by **3 units** over a
  short time. A client is counted as unstable if its swing interval crosses both the drop
  threshold and the return threshold; at hysteresis margin 0 the two thresholds coincide.
- **WN27.** In band steering, the narrow band is modeled with budget 58, the wide band with
  budget 46; the wide band's tier ceiling is double (`2 × tier`). This is the fiction of the
  range–speed trade-off.
- **WN28.** Band intent comes from the oracle: if a client would truly get more on the wide band,
  it belongs there. The oracle knows both bands' tiers.
- **WN29.** The steering rule sees only the narrow-band reading. The wide-band reading does not
  enter the decision; if it did, this would be a measurement, not a rule.
- **WN30.** The set is forty clients; the smallest measurable difference is **1/40 = 0.025**.

## Measurement

```python
"""Roaming and band steering: early handover and late handover are two separate errors."""
SEED = 20260811
POWER = 52
CORRIDOR = 50        # distance between the two access points, meters
SWING = 3             # the range the signal swings over a short time
TIERS = ((34, 6), (28, 4), (22, 2), (18, 1))


def generator(seed):
    d = seed % 2147483646 + 1

    def r(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return r


def clients(count=40, seed=SEED):
    r, pool = generator(seed), []
    for i in range(count):
        pool.append({"no": i + 1, "distance": 5 + r(41), "interference": r(13)})
    return pool


def signal(c, power=POWER):
    return power - c["distance"] - c["interference"]


def rate_tier(c, power=POWER):
    for threshold, tier in TIERS:
        if signal(c, power) >= threshold:
            return tier
    return 0


def gap(events, intent, mechanism):
    d = {"correct_admit": 0, "correct_deny": 0, "false_admit": 0, "false_deny": 0}
    for e in events:
        n, m = intent(e), mechanism(e)
        if n and m:
            d["correct_admit"] += 1
        elif not n and not m:
            d["correct_deny"] += 1
        elif m:
            d["false_admit"] += 1
        else:
            d["false_deny"] += 1
    return d


def handover_intent(c):
    """The client should belong to whichever point it is closer to."""
    return c["distance"] > CORRIDOR / 2


def handover(c, threshold):
    """Mechanism: if the signal from the point in hand drops below the threshold, let go."""
    return signal(c) < threshold


def unstable(c, threshold, margin):
    """If the signal's swing crosses both thresholds as it moves, the client ping-pongs."""
    return threshold + margin - SWING <= signal(c) <= threshold + SWING


pool = clients()
print(f"clients {len(pool)} | corridor {CORRIDOR} m | should belong to B "
      f"{sum(1 for c in pool if handover_intent(c))}")
print()

print(f"{'threshold':>9s} {'crossed':>7s} {'correct':>7s} {'false admit':>12s} "
      f"{'false deny':>12s}")
for threshold in (30, 26, 22, 18, 14):
    d = gap(pool, handover_intent, lambda c, t=threshold: handover(c, t))
    print(f"{threshold:9d} {d['correct_admit'] + d['false_admit']:7d} "
          f"{d['correct_admit'] + d['correct_deny']:7d} {d['false_admit']:12d} "
          f"{d['false_deny']:12d}")
print()

print(f"{'hysteresis margin':>17s} {'unstable clients':>17s}")
for margin in (0, 2, 4, 6):
    print(f"{margin:17d} {sum(1 for c in pool if unstable(c, 22, margin)):17d}")
print()

NARROW, WIDE = 58, 46


def rate_narrow(c):
    return rate_tier(c, NARROW)


def rate_wide(c):
    """Wide band: short range, double the tier ceiling."""
    return 2 * rate_tier(c, WIDE)


def band_intent(c):
    return rate_wide(c) > rate_narrow(c)


def steering(c, threshold):
    """Decision is made from the narrow-band reading alone; the wide band has not been tried yet."""
    return signal(c, NARROW) >= threshold


print(f"{'steering threshold':>19s} {'pushed to wide':>14s} {'correct':>7s} "
      f"{'false admit':>12s} {'false deny':>12s}")
for threshold in (44, 40, 36, 32):
    d = gap(pool, band_intent, lambda c, t=threshold: steering(c, t))
    print(f"{threshold:19d} {d['correct_admit'] + d['false_admit']:14d} "
          f"{d['correct_admit'] + d['correct_deny']:7d} {d['false_admit']:12d} "
          f"{d['false_deny']:12d}")
print()
print(f"clients that truly belong on the wide band: {sum(1 for c in pool if band_intent(c))}")
print("tier lost by false admits (no, narrow tier, wide tier):")
print([(c["no"], rate_narrow(c), rate_wide(c)) for c in pool
       if steering(c, 36) and not band_intent(c)])
```

```
clients 40 | corridor 50 m | should belong to B 19

threshold crossed correct  false admit   false deny
       30      27      32            8            0
       26      27      32            8            0
       22      19      36            2            2
       18      17      36            1            3
       14      11      32            0            8

hysteresis margin  unstable clients
                0                 9
                2                 9
                4                 6
                6                 2

 steering threshold pushed to wide correct  false admit   false deny
                 44              7      38            0            2
                 40              9      40            0            0
                 36             13      36            4            0
                 32             13      36            4            0

clients that truly belong on the wide band: 9
tier lost by false admits (no, narrow tier, wide tier):
[(1, 6, 4), (11, 6, 4), (23, 6, 4), (36, 6, 4)]
```

## The Threshold Shifts the Balance

The top table turns a single dial, and two columns move in opposite directions.

At threshold 30, false admit is 8, false deny is 0. Clients let go while still close to A; eight
of them have crossed to B while still on A's side. No one is late, because the threshold is so
high that everyone leaves ahead of time.

At threshold 14, the opposite: false admit is 0, false deny is 8. No one leaves early, and eight
clients stick to A while already close to B. A client that sticks both keeps its own tier low and
holds the medium for longer; the shared-time calculation from the first lesson shows up here
again.

The three middle rows show where the balance shifts: **2/2**, **1/3**, and, in between, 30 and 26
giving the same row. The two rows coming out identical is a resolution effect of the fiction — no
client's signal strength falls between 26 and 30, so moving the threshold across that interval
changes no decision at all. The measurement band (WN30, **0.025**) cannot see this gap.

The rule to read is: **early handover and late handover are two ends of the same dial.** An
operator who receives only sticking complaints raises the threshold and ends the sticking; the
moment it ends, early handover begins, and that too is a complaint — a different one, because
early handover produces short outages while sticking produces continuous slowness. A measurement
that does not count the two directions separately can do nothing but move the dial from one end
to the other.

## Ping-Pong and Hysteresis

The middle table counts a different flaw. Signal strength is not constant; it swings by a few
units over a short time (WN26). A client sitting right at the threshold's edge makes the decision
to let go because of this swing, then returns, then lets go again. This is called **ping-pong**,
and it produces a frame loss on every round.

In the single-threshold mechanism (hysteresis margin 0), 9 of the forty clients sit inside this
band. Raising the margin to 4 drops the unstable clients to 6, to 6 drops it to 2. The mechanism
is this: the drop threshold and the return threshold are separated; a client lets go only when
the signal falls below the lower threshold, and returns only when it rises above the upper one.
The gap between them absorbs the swing.

The margin's cost is read in the table above. Holding the drop threshold fixed and raising the
return threshold makes returning harder: a client that went to the wrong point stays there
longer. **Hysteresis reduces instability and lengthens the life of a wrong decision.** Margin 2
changing nothing is the same resolution problem — a 2-unit margin sits below a 3-unit swing and
removes no client from the band.

## A Decision Made from a Wrong Reading

The bottom table measures band steering. Of the forty clients, 9 truly belong on the wide band;
the steering rule tries to guess this from the narrow-band reading.

At threshold 44 the rule pushes 7 clients: false admit 0, false deny 2. The rule is cautious; it
sends no one to the wrong place, but it fails to run two clients on the band they deserve. At
threshold 36, 13 clients are pushed: false admit 4, false deny 0. These four are listed in the
last row and their cost is plain — they would have gotten tier 6 on the narrow band and get 4 on
the wide band. The rule slows down exactly the clients it pushed to "improve."

The threshold 40 row gives 40/40 correct: 0 false admit, 0 false deny. This row has to be read
carefully. It does not show that the rule is correct; it shows that, in this set, that particular
cut happens to coincide with intent. The rule still does not measure what it wants to measure —
it never sees the wide-band reading (WN29). The same threshold could well be wrong on a
different client distribution, because a proxy measurement's accuracy depends on the set it is
measuring.

And threshold 36 and 32 giving the same row is the counterpart of the 30–26 pair in the table
above: there are intervals where moving the threshold changes no decision at all. It cannot be
assumed that every time a threshold is "adjusted," something has actually changed.

## Summary

- The client makes the roaming decision; the only mechanism in the network's hands is
  thresholds. The measure is therefore the gap between the network's intent and the client's
  decision.
- As the handover threshold drops from 30 to 14, false admit is **8, 8, 2, 1, 0**, false deny is
  **0, 0, 2, 3, 8**. Early handover and late handover are two ends of the same dial.
- In the single-threshold mechanism, 9 clients sit in the ping-pong band; at hysteresis margins
  4 and 6 this drops to 6 and 2, but the life of a wrong decision lengthens.
- The band steering rule never sees the wide band's reading; at threshold 44, 2 clients do not
  go to the band they deserve, at threshold 36, 4 clients are dropped from tier 6 to tier 4.
- There are intervals where changing the threshold changes no decision (30–26 and 36–32); that
  an adjustment was effective cannot be inferred from the adjustment having been made.

## Next Step

Up to this point, the medium determined who got through. Distance, interference, budget, and
threshold — all four were physical or numerical quantities, and none of them asked who the
client was. Everyone standing inside the cell associated, no one standing outside could
associate; distance was what drew the boundary.

So what happens if an **identity condition** is added to the crossing? Then association passes
two tests: is the client close enough, and is the client acceptable? The next lesson builds this
second test and counts the same two directions there too — the identity condition admits some
clients it should not, and denies some it should not.
