vibeapp.win

What broke

Known rate limit handled only in the smoke test, not production

Claude hit the broker's order rate limit in a test, added a pause to the smoke test and wrote about it in a commit, but left the production path unprotected. On the stock app's first live cycle, three of six orders were rejected with HTTP 429.

The short version

What went wrongClaude hit the broker's order rate limit in a test and added a pause only to the smoke test. On the first live cycle, three of six orders were rejected with HTTP 429.

How we found itThe owner reported rejected Saxo purchases, and the order log showed the 429s. The fix commit admits the limit was already known.

The rule nowA known limitation handled only in the test tool is not handled, so the fix goes into the production path: an engine pause between orders and a 429 retry.

This post documents something Claude got wrong. The claim rests on a commit or a file, not on memory. All of Claude's mistakes

On 5 August 2026 at 17:00, the stock app traded for the first time against a real broker, Saxo. It sent six orders in about two seconds. Three were submitted and three were rejected with HTTP 429.

Nothing guarded against the rejections. They came from a rate limit that Claude had already hit that same morning, written about in a commit message, and handled only in the test tooling.

What happened

The fix commit records the first live cycle as it came back from the broker:

  • 17:00:01: order 1 submitted
  • 17:00:02: order 2 rejected, HTTP 429 RateLimitExceeded
  • 17:00:02: order 3 rejected, 429
  • 17:00:02: order 4 submitted, later filled
  • 17:00:02: order 5 submitted
  • 17:00:03: order 6 rejected, 429

That is three valid trading decisions thrown away. No safety check stopped them. The broker’s gateway turned them down because they came too fast.

How it was found

The commit opens with the owner’s report, translated from Norwegian: “the Saxo purchases are getting rejected.” The order log above came out of the investigation that followed.

What Claude got wrong

The commit is written in the first person, and it is blunt about it. Its title, translated: “three of six orders rate-limited away – I knew about the limit.”

According to the commit, earlier that morning Claude ran an idempotency test against the broker and hit exactly this limit. In that commit message it noted that “the order endpoint is MORE TIGHTLY rate-limited than the lookups” (translated) and added a five-second pause to the smoke test. In its own words in the fix commit: “Production path I left alone.”

So the knowledge was there, and it was written down. The fix just went into the wrong place. The pause went into the smoke test. The production engine did not pause, so the first time it sent a real batch, it ran into the same wall.

We don’t have the morning commit in front of us for this post. What we know about it comes from how the fix commit describes it, along with a comment in the new test file that says the same thing: the limit was hit during the idempotency test, and the pause went into the smoke test only.

Two fixes

The fix commit changes two things.

1. The engine pauses between orders. The live executor now waits 1.2 seconds before each order when the broker is real. The commit’s reasoning: avoiding the limit is better than waiting your way through it. The paper broker skips the pause, since it never leaves the machine and has no rate limit to respect.

2. The broker adapter retries on 429. If the broker answers 429, the adapter tries the same order again, up to three more times. If the response includes a Retry-After header, it waits that long. If not, it backs off, starting at 1.5 seconds and doubling each time. Any other response ends the loop right away.

The pause is meant to keep the app under the limit. The retry is a safety valve for when it slips over anyway.

Why the retry doesn’t break an existing rule

The stock app already had a deliberate rule: no automatic retry on orders. The adapter retries freely on read requests but never on order placement. The reason is that a missing response tells you nothing about whether the broker received the order. Retry after a timeout and you may have placed the same order twice.

The commit explains why a 429 is different. It isn’t a missing response. It is a definite refusal from the gateway, sent before the order is processed. So retrying a 429 is safe in a way that retrying a timeout never is.

There is a second layer as well. Each order carries an external reference that stays the same across retries. If the broker did somehow accept the first attempt, the second gets a 409 conflict instead of becoming a duplicate. The commit notes that this behaviour had been shown in the idempotency test that same morning, and calls idempotency “the safety net that makes this retry defensible” (translated).

To keep it that way, a new test checks that the order body, including the reference, is built once, before the retry loop. If someone later rebuilt it inside the loop with a fresh client order ID, a duplicate would get through as a new order. The test is there to catch that.

Tests added

The commit adds four tests in a block about order rate-limiting. They check that:

  • the adapter retries on 429
  • it retries on nothing else, timeouts included
  • the order body is built before the retry loop, so the external reference can’t change between attempts
  • the live executor pauses between orders, and only for a real broker

The commit reports 931 tests passing, four of them new, and zero lint errors.

Apart from the rate limit, the commit says the rest of the cycle worked: the order that filled showed up as a position, cash dropped by the right amount, and the position was valued at the app’s own prices.

What we take from it

This wasn’t a gap in knowledge. The limit had been found, measured roughly (“tighter than the lookups”) and written down the same day. The gap was where the fix went. A pause in a smoke test protects the smoke test. It does nothing for the code path that places real orders.

It’s an easy mistake to make, because a passing smoke test feels like proof that the problem is handled. It only proves the problem is handled in the smoke test.

The rule

The commit states it directly, translated: a known limitation that is handled only in the test tool is not handled. When a test turns up a real-world constraint, the fix goes into the production path. For this one, that meant the engine pauses between orders and the adapter retries on 429.