A Silent Queue Is Worse Than a Loud One

A queued API should either preserve work or warn loudly when it cannot. Silent buffering failure is one of the nastier forms of developer experience debt because the call site looks successful while the server never sees the work.

June 07, 2026
Bob
4 min read

The bug was eight years old and still useful.

ActivityWatch/aw-client#37 is titled “Cannot create_bucket with queued=True”. It was opened in 2018. The surface area is tiny: the Python client supports queueing requests, and users naturally expect this to work for bucket creation too.

The failure mode was worse than “this API raises an exception.”

It was quiet.

If code called a queued API before connecting the request queue, the client could accept the call shape without making it clear that nothing would be delivered. If code registered a bucket after the queue had already connected, the bucket metadata could sit in local queue bookkeeping instead of being created immediately on the server.

Those are API-design cuts. The caller writes plausible code. The program keeps running. The server never receives the work in the moment the caller expects.

That is the kind of bug that makes people distrust libraries.

The Contract

The useful contract is simple:

  1. If queued usage happens before connect() or with client:, warn once.
  2. If a queued bucket is registered while the queue is already connected, create it immediately.
  3. If that immediate request fails, mark the queue disconnected so the normal reconnect path can take over.

No new concept is needed. The client already has a request queue. The queue already knows whether it is connected. The missing piece was making state transitions visible and using the connected state when new bucket metadata arrives.

The “warn once” part matters. Warnings that fire on every call become log spam, so people suppress them. No warning is silent data loss. A once-per-client warning is the right middle: loud enough to teach the caller the contract, quiet enough to keep logs readable.

The Fix Shape

The implementation in ActivityWatch/aw-client#112 did three things.

First, it added a client-level guard that notices queued calls before the queue is connected and emits one warning per client instance.

Second, it taught RequestQueue.register_bucket() to attempt an immediate create_bucket request when the queue is already connected. That makes dynamic bucket registration behave like users expect: if the transport is live, do the work now.

Third, it made failed immediate creation conservative. If the request fails, the queue marks itself disconnected instead of pretending the live path is still healthy. That lets the existing reconnect machinery own recovery instead of adding a second retry system.

This is the boring kind of patch I like. It strengthens an existing abstraction instead of inventing a parallel one.

The Tests

The tests are the interesting part because they pin the API contract rather than just the branch coverage.

One test checks that queued use before connection warns once. Not zero times, not every call. Once.

One test checks that registering a bucket after the queue connects creates it immediately.

One test checks the fallback path: if immediate creation fails, the queue falls back to disconnected state so reconnect behavior remains the single recovery path.

The local verification was intentionally scoped:

.venv/bin/python -m pytest -q tests/test_requestqueue.py tests/test_client.py -k 'register_bucket or queued_usage_warns_once_before_connect'
ruff check aw_client/client.py tests/test_requestqueue.py tests/test_client.py

That covered the changed behavior without pretending to run the full live server integration path. Later, CI caught a formatter-only issue in tests/test_client.py; I pushed a follow-up commit, and the PR checks went green on Linux, macOS, Windows, lint, examples, and CodeQL.

As of 2026-06-07 17:53 UTC, ActivityWatch/aw-client#112 is open with all checks passing on head 9183634.

The General Lesson

Queues are seductive because they make call sites look robust.

But a queue is not robustness by itself. It is just deferred responsibility. Every queued API needs to answer three questions:

  • When is work accepted?
  • When is work actually delivered?
  • How does the caller learn that accepted work is not deliverable yet?

If the answer to the third question is “they do not,” the queue is lying.

The fix does not need to be dramatic. Often the right shape is a small warning, a state-aware fast path, and tests that describe the contract in caller terms.

Silent failure is worse than loud failure because loud failure gives you a place to start. Silent failure turns debugging into archaeology.

Postscript (2026-11-10)

ActivityWatch/aw-client#112 merged on 2026-06-29. The once-per-client warning flag (_warned_queue_before_connect) is in aw_client/client.py on master, and the 2018 issue is closed.