43 tests. That is what it took to be confident in a state machine that decides whether a posting account can handle more writes today.
The problem: fresh X accounts cannot go from zero to full throughput on day one. The platform watches your write velocity and punishes sudden spikes with code 226 (write limit exceeded). Ignore enough 226s and you get a 14 day penalty stamped on the account. So you need a warmup module that advances cautiously, reads the platform's error signals, and rolls back when things go wrong. A hardcoded delay is not enough. You need a state machine.
The design
warmup.py is built around a single evaluate() function that the publish loop calls before deciding whether to post. Every call checks four gates before advancing to the next phase:
- Dwell: enough time has passed in the current phase
- No sustained 344s: a sustained stream of "not found" errors means something is broken at the read level
- Health: the overall error rate is acceptable
- Impression gate: posts are actually landing, not silently failing
All four pass and the phase advances, the write ceiling rises, and the account gets more room. Any one fails and the phase stays put.
The rollback logic
If evaluate() detects a code 226 anywhere in the trailing 7 days, it rolls the phase back by one. That is the first penalty. If the same phase triggers a rollback a second time, evaluate() does not just roll back again: it halts the entire warmup and fires an alert. The account stops posting until a human looks at it.
Why halt on the second rollback rather than keep rolling back indefinitely? One rollback might be noise. Two rollbacks at the same phase is a signal that the ceiling for that phase is genuinely too high for this account. Continuing would oscillate forever and accomplish nothing. Halting forces a decision.
over_ceiling() has three states, which I almost got wrong
False when warmup is disabled (not in effect at all, so ceiling is not a constraint)
True when halted (blocked regardless of write count)
Otherwise, writes_today >= ceiling
The middle case is the one to get right: a halted warmup means over_ceiling() returns True even if writes_today is zero. The publish loop checks this function and must treat True as a hard stop, not a "try again later." Getting that wrong means a halted account silently resumes posting.
WARMUP_PHASES = None in config exists so tests and manual overrides can disable warmup entirely without touching state. Convenient, but it also means the module has a silent off switch that is easy to forget about in production.
The test suite
43 tests cover every public function and the full evaluate() state machine: normal advance, dwell not met, each gate failing independently, first rollback, second rollback at the same phase, halt condition, and reset. Writing the tests before wiring evaluate() into the publish loop forced the edge cases into the open before they could cause a real 14 day penalty on a live account.
What I would do differently
The 7 day trailing window for code 226 detection is a guess. Long enough to catch a real pattern, short enough that one bad day does not follow the account for weeks. I would instrument it in production and likely shorten it to 3 to 5 days once there is real signal.
The halt on second rollback is the most conservative call in the module. There is a legitimate argument for counting total rollbacks across all phases rather than counting per phase. I chose per phase counting because it ties the halt directly to a specific ceiling being repeatedly rejected by the platform. That reasoning is sound on paper. Whether it holds in practice is an empirical question I will answer after the first few weeks of live data.
The startup log line for whether warmup is active does not exist yet. It should. A silent off switch with no log trace is a debugging nightmare.
The state machine itself is not complicated. The 43 tests make sure it stays that way.
Top comments (0)