Turbo Catch-Up: Draining a Backlog After an Outage
Most engineers learn backpressure the moment a fast producer overwhelms a slow consumer: you slow the producer down. But there's a mirror-image problem that gets far less attention. When a device has been offline — a home router rebooted, the ISP hiccuped, the WiFi dropped for an hour — it comes back holding a mountain of unsent data that was never lost, just stranded.
Now the consumer (the cloud uploader) isn't too slow; it's too polite. If it keeps sipping at its normal leisurely rate, the backlog takes hours to clear, and fresh data keeps piling on behind it. Our bowl's uploader solves this with Turbo Catch-Up: it measures how far behind it is and deliberately accelerates — bigger batches, no pauses — until it's caught up. This is a look at backpressure in reverse.
The Concept: Adaptive Throughput, Not a Fixed Rate
A naive uploader sends at one hard-coded cadence — say, a batch of 100 records every second — no matter what. That's fine in steady state, where data trickles in roughly as fast as it's sent. It falls apart after an outage.
The key idea is that the right send rate depends on how far behind you are. When the backlog is small, sip gently and leave the network and CPU alone. When the backlog is large, the polite pace is actively harmful — it guarantees you stay behind. So instead of a constant rate, the uploader reads its own backlog on every cycle and picks a throughput tier to match.
The tiers are just thresholds on the number of unsent records, with a bigger batch and shorter (or zero) pause at each level:
106 #define TURBO_MAX_THRESHOLD 50000 // Extreme backlog (>1 hour outage)
107 #define TURBO_HIGH_THRESHOLD 10000 // Large backlog (>8 min outage)
108 #define TURBO_MEDIUM_THRESHOLD 1000 // Moderate backlog
110 // BATCH SIZES BY MODE
111 #define BATCH_SIZE_NORMAL 100 // Normal operation
112 #define BATCH_SIZE_CATCHUP 150 // Moderate catch-up
113 #define BATCH_SIZE_TURBO 300 // High-speed catch-up
114 #define BATCH_SIZE_TURBO_MAX 500 // Maximum speed catch-up
Four gears. Normal cruising, moderate catch-up, turbo, and turbo-max. The uploader shifts between them automatically based on one number: how much unsent data is sitting in the queue.

Why It Matters: A Polite Uploader Never Recovers
Imagine the bowl samples weight continuously and buffers every reading to a local database, so nothing is ever lost offline. After a one-hour outage, tens of thousands of unsent readings are waiting.
At the normal pace — a batch of 100 with a one-second pause between batches — clearing 50,000 records would take on the order of eight minutes of pure send time, and that's if no new data arrived. But new data keeps arriving the whole time. The queue drains slower than real life fills it, so the device limps along permanently behind, its "live" dashboard showing data that's already stale.
The fix isn't a faster network or a bigger buffer. It's letting the uploader recognize the situation and change its own behavior — trading its steady-state gentleness for aggression exactly when, and only when, it's needed. That's adaptive throughput control, and it's the difference between a device that recovers in seconds and one that never quite catches up.
How It Works: Measure, Then Shift Gears
Every cycle begins by asking a single, cheap question of the local queue — how many records are still unsent?
826 static int get_backlog_size(void) {
827 pthread_mutex_lock(&db_mutex);
828
829 const char *sql = "SELECT COUNT(*) FROM readings WHERE sent=0";
830 sqlite3_stmt *stmt;
831 int backlog = 0;
832
833 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
834 if (sqlite3_step(stmt) == SQLITE_ROW) {
835 backlog = sqlite3_column_int(stmt, 0);
836 }
837 sqlite3_finalize(stmt);
838 }
839
840 pthread_mutex_unlock(&db_mutex);
841 return backlog;
842 }
That count is the whole control signal. There's no complex queue-theory model, no moving average, no PID loop — just "how far behind am I right now?" answered directly from the durable store that already holds the data.
With the backlog in hand, the uploader shifts gears. It defaults to the gentle setting and escalates only if the backlog crosses a threshold, checking the most extreme case first:
1206 if (backlog > TURBO_MAX_THRESHOLD) {
1207 // EXTREME BACKLOG (>50k records, ~40 min outage)
1208 batch_size = BATCH_SIZE_TURBO_MAX;
1209 sleep_interval = 0;
1210 mode = "TURBO-MAX";
1211 stats.turbo_mode_activations++;
1212
1213 } else if (backlog > TURBO_HIGH_THRESHOLD) {
1214 // LARGE BACKLOG (>10k records, ~8 min outage)
1215 batch_size = BATCH_SIZE_TURBO;
1216 sleep_interval = 0;
1217 mode = "TURBO";
1218 stats.turbo_mode_activations++;
1219
1220 } else if (backlog > TURBO_MEDIUM_THRESHOLD) {
1221 // MODERATE BACKLOG (>1k records)
1222 batch_size = BATCH_SIZE_CATCHUP;
1223 sleep_interval = 0;
1224 mode = "CATCH-UP";
1225 }
Two levers move together. The batch size grows — more records shipped per network round trip, so the fixed per-request overhead is amortized across far more data. And the sleep interval drops to zero — no deliberate pause between batches when there's a mountain to move. In the top gear it's 500 records per request, back to back, as fast as the link allows.
There's a subtle discipline here worth calling out. The escalation is one-directional per cycle and self-correcting: because the tier is recomputed from the live backlog every single iteration, the uploader automatically drops back down through the gears as the queue shrinks — turbo-max to turbo to catch-up to normal — without any separate "slow down" logic. The same measurement that triggers acceleration also ends it.

In Practice: Bounded Aggression
Acceleration without limits is its own failure mode, so Turbo Catch-Up is deliberately bounded on both ends. The top gear caps at 500 records per request — large enough to amortize overhead dramatically, small enough that a single failed request doesn't cost too much to retry. And the tiers are coarse thresholds with wide gaps, so the mode doesn't flap rapidly between gears as the backlog wobbles near a boundary.
The database read that drives it all is also cheap by design: a single indexed count of unsent rows, taken under a short lock, once per loop. The control signal costs almost nothing to compute, which is what makes it safe to re-evaluate constantly rather than occasionally.
The system even keeps score. A turbo_mode_activations counter records how often the device had to drop into a catch-up gear — a quiet health signal that, over a fleet, tells you which devices live on flaky connections and how bad their outages really are. Catch-up isn't just a recovery mechanism; it's also a diagnostic.

Why It Matters at Hoomanely
Hoomanely is reinventing healthcare for pets — replacing reactive, imprecise care with continuous, clinical-grade monitoring that catches problems early. Our devices form a Physical Intelligence ecosystem: sensors fused at the edge, feeding the Biosense AI Engine that turns raw signals into personalized, preventive insights.
Those insights depend on timely, complete data, and home networks are anything but reliable. A feeding station that captures every gram of every meal is only useful if that record reaches the cloud while it still matters — a day-old backlog trickling in an hour at a time undermines the whole promise of continuous monitoring. Turbo Catch-Up is what lets the device shrug off an outage: it buffers everything locally, then sprints to catch up the instant it's back online, so the pet's health timeline snaps back to real-time instead of lagging for hours.
Our guiding principle is that every signal matters and every detail counts. Making sure a stranded backlog drains fast — without hammering a fragile connection needlessly in steady state — is a small piece of adaptive engineering that keeps the data, and the insights built on it, trustworthy and current.
Key Takeaways
- Backpressure has an inverse. After an outage the consumer isn't too slow, it's too polite — a fixed gentle rate can never catch up to a large backlog.
- Let the backlog pick the rate. Measuring how far behind you are, every cycle, is a cheaper and more responsive control signal than any fixed schedule.
- Move two levers together. Grow the batch size to amortize per-request overhead, and drop the inter-batch pause to zero, only while a backlog exists.
- Make it self-correcting. Recomputing the tier from the live backlog each loop means the system down-shifts automatically as it recovers — no separate slow-down logic.
- Bound the aggression. Cap the top batch size and space the tier thresholds widely so catch-up is fast but never reckless or flappy.
Author's Note
Turbo Catch-Up runs on the compute module inside Hoomanely's smart feeding station, draining the local reading queue back to the cloud after every network hiccup. It's a reminder that resilience isn't only about storing data through an outage — it's about getting it current again the moment you can. Buffer everything, then sprint to catch up: that's how a device on ordinary home WiFi still delivers a health record you can act on today, not tomorrow.