It Does Not Ask — Why Polling Is Worse Than Wasteful

5 min
GoalLast in the mcp_client course. Subscribe, get told, then read. That polling costs resources is secondary — the real problem is that when a value changes twice between two polls, the middle is never seen.

Through part 4, knowing the latest value meant asking — every second, or when a person refreshed.

Subscribe

client.onResourceUpdated((uri) {
  // The notification says only that it changed. Read to learn what to.
  seen.add(uri);
  stdout.writeln('notified: $uri');
});

await client.subscribeResource('desk://waiting');

Then make the value change.

await client.callTool('desk.admit', {'count': 1});
subscribed to desk://waiting
notified: desk://waiting
after the notification, reading gives {waiting: 2}
notifications received: 1

The notification carries only the uri

The handler receives one uri. How many are waiting does not arrive. Server part 6 covered why no value rides along; on the receiving side it means you have to read.

final now = await client.readResource('desk://waiting');

One more round trip looks like a loss, and it is always right. However many notifications arrive and in whatever order, the value at the moment you read is the current one.

The real problem with polling

Polling is usually rejected on cost. Knocking on the server every second is wasteful.

That is secondary. The real problem is that you cannot see between two polls.

09:00:00    poll → waiting 3
09:00:00.4  two admitted → waiting 1
09:00:00.7  one arrives  → waiting 2
09:00:01    poll → waiting 2

The screen went from 3 to 2. It never saw the moment it was 1. If only the final value matters, fine — but a rule like "alert when it reaches zero" will never fire.

Shortening the interval does not remove this. It narrows it. And the narrower it gets, the more the waste grows.

Unsubscribe when done

await client.unsubscribeResource('desk://waiting');

When the screen closes, drop the subscription. Leave it and the server keeps sending to nobody.

The ESP32 in the real-board piece put this inside the screen definition — subscribe on onReady, release on onDestroy. With the lifecycle written into the screen, the client cannot forget.

Verification — polling fails

grep -q 'notified: desk://waiting' captures/s5.txt || die "no notification arrived"
grep -q 'notifications received: 1' captures/s5.txt || die "wrong notification count"
grep -q 'reading gives {waiting: 2}' captures/s5.txt || die "the read after the notification was stale"

# Polling would show up as a timer; the claim is that there is none.
grep -qE 'Timer|periodic' bin/step5.dart && die "it is polling, not subscribing"

A Timer or periodic in the code fails it. Because from the result alone, polling and subscribing look identical — both put 2 on the screen.

Counting notifications is for the same reason. Zero means none arrived; several means a duplicate subscription.

Five steps done

step1  connected to Course 1.0.0
step2  tools: desk.admit
       resources: ui://desk, desk://waiting
step3  admit 1 -> {waiting: 2}
       admit 99 -> isError=true "only 2 waiting"
step4  ui://desk — 546 B, 6 lines, type "page", title "Desk"
step5  notifications received: 1 · after the notification, reading gives {waiting: 2}

5 steps · each checked against its own claim · driven against the course server

There is no mock server. All five steps start and attach to the server built in the server course. So when the two tracks drift, it shows up here — and in part 3, it did.

Run it yourself

cd content/sample/course-client
dart pub get
bash verify.sh            # all five steps
dart run bin/step5.dart

What to take

  1. onResourceUpdated then subscribeResource — hang the handler before subscribing
  2. Read after the notification — it carries only a uri
  3. A no-polling check — the results look the same, so read the code

Next track

Server and client are standing. Next is the screen itself — how one blob of JSON becomes one.

Run the sample

cd course-client
dart pub get
dart run bin/step5.dart