Work in progressthis site is being built. Some sections and figures are placeholders.
p. 08
Plate II
Sumeru Chatterjee.

Plates / 002 · Iterable Mobile SDK

Plate II · 002 2024 — Present Lisbon · Remote Enterprise SaaS · Mobile SDK platform

Iterable Mobile SDK.

I make the SDKs that millions of phones depend on boringly reliable — across iOS, Android and React Native.

§ 00At a glance

The parts of the app you never see.

Staff engineer and technical owner of Iterable's mobile SDK platform — the integration layer for push, in-app messaging and real-time analytics that thousands of apps embed. My work lives in the invisible, load-bearing parts: authentication, threading, app lifecycle, offline queues, crash and ANR prevention. The win is that nothing broke. The failure mode is a phone call from a strategic customer.

120+

pull requests authored
across four mobile SDKs

100M+

devices under the release-health
monitoring I designed

4

runtimes — iOS, Android,
React Native, Flutter

0

the goal — crashes that didn't
happen, escalations that didn't recur

§ 01The spine · Reliable auth at scale

When a token expires, nothing should break.

That turned out to be hard.

Every authenticated SDK call carries a short-lived JWT. When it expires the server returns a 401 — and the SDK has to fetch a new token and retry, without losing data, hammering the customer's backend, or deadlocking.

The old behavior had a quiet, expensive bug. When several calls fired at once on app startup with an expired token, only the first triggered a refresh. The rest saw a refresh already in flight, dropped their retry callbacks, and were never re-sent — lost events, failed device registrations, missing messages. And because offline-queued requests stored a stale token snapshot taken when they were enqueued, replaying them after reconnect failed too.

I designed and shipped the fix as a sequenced, feature-flagged rollout — six coordinated changes to the offline task pipeline, each independently testable and reversible.

FIG. II.A · Auth-retry state machine
Normal flow
authenticated calls drain the offline queue
401 — JWT expired
Retain & pause
keep the task in the queue; set authPaused
Unauthenticated work bypasses →
trackEvent / trackPurchase keep flowing
trigger refresh
Token refresh in flight
single refresh, not one per call
new token received
Gate · resume only when BOTH ready
fresh token ✓ AND connectivity ✓ · 3s reconnect debounce
both ready
Replay queued work, in order
with the new token, nothing dropped
Fig. II.A The offline-queue state machine. A 401 retains and pauses authenticated work rather than dropping it; unauthenticated calls keep flowing; the queue resumes only when a fresh token and connectivity are both present, debounced against a flapping network. Redrawn from the design — not a screenshot.
01

Retain and pause

On a 401, keep the work in the queue and pause the runner instead of silently dropping it. A distinct authPaused state, separate from the network-paused state — because the two have different resume conditions. PR #1004

02

Keep unauthenticated work flowing

Calls that don't need a token shouldn't wait behind one. Event and purchase tracking bypass the auth pause entirely.

03

Resume only when both are ready

A fresh token and connectivity, with an asymmetric debounce — pause instantly on disconnect, wait three seconds before trusting a reconnect — so a flickering network can't thrash the queue. PR #1011

04

Rethink what "permanent" means

Only client errors are permanent. Server errors, timeouts and dropped connections are transient and get retried. The policy fails open: an unknown error retries rather than discards. PR #1012

It landed off-by-default behind a flag, so customers who hadn't opted in saw byte-for-byte identical behavior — zero risk for everyone else while it proved out. The coordination is notification-based rather than direct coupling, and every state mutation runs on the persistence layer's own serial queue — reusing the database context as the concurrency primitive instead of bolting on a separate lock.

Unknown or new error types default to retry — reducing the risk of data loss.

— from the pull request

Kicker

The design held up well enough that it became the team's standard mobile-systems interview question.

§ 02War stories

Three times the bug was a phone call.

Here's the detective work.

Story 1 · iOS deep linking — several high-growth consumer apps

Fast networks were failing more often.

Deep links were failing intermittently on iOS — users tapped an email or SMS link and the app opened to the wrong place, or not at all. The maddening part: it was worse on good connections.

The SDK follows a link's redirect just far enough to capture attribution, then cancels it. Cancelling makes the network layer report an error — and the old code checked for that error first, bailing out and throwing away the destination URL it had already successfully captured a moment earlier. On a fast network the request finished quickly enough to trip the cancellation error every time; on a slow one it slipped through. Better connectivity, more failures.

The fix was to reverse the order: use the data if we have it, and only treat the error as fatal if we genuinely came back empty-handed. A handful of lines.

Ironically, clients with better network conditions were more likely to experience failures.

— from the diagnosis

Story 2 · Android auth & lifecycle — a top-10 global travel brand

One customer was sending us 10× the traffic.

Their Android apps were generating ten times the request volume of their iOS apps, straining infrastructure. The culprit was a timer. Android's auth manager used a timer that runs on its own thread, independent of the app lifecycle — so it kept firing token refreshes even while the app was backgrounded. iOS timers are tied to the run loop and naturally pause when the app suspends.

I made the Android timer lifecycle-aware: clear it on background, re-evaluate on foreground. Ninety-five lines, and the platforms finally behaved the same way. The same customer's app-launch ANRs traced back to the same area — heavy crypto and storage work blocking the main thread at startup — which I fixed by moving initialization off the main thread entirely.

A minimal, focused fix that addresses the specific issue without introducing unnecessary complexity.

— from the pull request

Story 3 · Analytics integrity — a major European travel & membership brand

Inflated metrics nobody could reproduce.

A customer's mobile analytics broke: millions of sessions logging zero seconds, push-open numbers inflated. It took the team weeks to find, partly because nobody could reproduce it internally — and partly because the root cause was intended behavior nobody had documented. "Silent" wake-up pushes were being counted as real opens. I owned the workstream to identify and correct the erroneously-tracked events and clean up the customer's data.

The deeper lesson stuck: the incident was a direct argument for building a real end-to-end test harness and real observability, so the next regression would be caught in CI, not in a customer's dashboard. Both became projects I led — and they're in the next section.

§ 03What I own

Whole subsystems, end to end.

FIG. II.B · Same bug, four runtimes
iOS
Authored
Android
Authored
React Native
Bridged
Flutter
Led / triaged
Fig. II.B Cross-platform parity means the same behavior — down to naming and API shape — across four runtimes. A fix on iOS is incomplete until Android matches it.

Auth & offline reliability

A

The JWT refresh-and-retry pipeline across iOS and Android. The deepest thread of my work — and the one that became the interview question.

swift-sdk #1014

Threading, ANRs & lifecycle

B

Moving heavy work off the main thread; fixing the data races the thread sanitizer caught in the SDK's core async primitive.

swift-sdk #1063 · race fix

Test infrastructure

C

A business-critical integration-test framework that drives the real backend end-to-end — now the team's pre-release gate. ~15,000 lines.

swift-sdk #918 · the framework

Cross-platform parity

D

The same behavior across four runtimes, down to a public-API rename done with backward-compat shims and no breaking version bump.

swift-sdk #922 · a parity rename

Observability

E

The release-health dashboard the team checks after every ship — error rates, version adoption and traffic across 100M+ devices, with alert thresholds and a runbook.

Security

F

Closed a credential-tampering (TOCTOU) gap in the Android login path by changing the data-flow shape — capture, store, use in one synchronous chain.

android-sdk #957 · security fix

§ 04Artifacts

What the work leaves behind.

The release-health dashboard. After the metrics incident, "did we just break something?" needed a one-glance answer. I designed a cross-platform dashboard tracking error rates, version adoption and traffic across 100M+ devices, with alert thresholds and a documented playbook for reading it.

FIG. II.C · Release-health dashboard (stylized)
Requests / 5 min
6.2M
Nominal
401 rate
0.04%
Below threshold
Crash-free
99.94%
Healthy
ANR rate
0.11%
Watch
401 rate after release — iOS 6.7.0alert threshold ·· · spike = token-refresh regression
release spike → caught in CI next time
Fig. II.C A stylized reconstruction of the dashboard's logic — never a real screenshot. A spike in 401s after a release is one of the most common SDK regression patterns; the threshold line is there to make it a five-second read.
FIG. II.D · Release cadence under my tenure
iOS
6.5
6.6
6.6.4
6.7
Android
3.5
3.6
3.7
3.8
React Native
1.3
2.0
3.0
early 2025mid 2026
Fig. II.D Steady, predictable shipping across three platforms — two major versions each on React Native, with iOS and Android moving in lockstep behind it.
FIG. II.E · Judgment isn't measured in lines
Integration-test framework — built the pre-release gate+15,054
Auth-retry epic — six coordinated changes+4,129
Background init — killed an ANR+2,466
Background auth-refresh — cut traffic 10×+95
Deep-link race — fixed it on fast networks+19
KeyStore crash fallback — stopped a launch crash+7

— two of the highest-impact fixes here are under 100 lines. Green = small surface, large blast radius.

Fig. II.E The biggest builds and the smallest fixes, to scale. A 95-line lifecycle change cut a strategic customer's request volume tenfold; a 7-line fallback stopped a launch crash on a class of devices. Staff work is knowing which line to change.

§ 05How I work

In my own words.

Four lines pulled verbatim from pull requests and runbooks — how I think about a change before I make it.

It is mathematically impossible for this change to break existing functionality.

On proving a new code path can't touch the old one — a regression-safety argument, not a hope.

This implementation prevents ANRs during initialization only.

On being precise about the limits of a fix instead of overclaiming it.

Serialize all state behind a private queue — and fire callbacks outside the critical section, so re-entrant registration can't deadlock.

On getting concurrency right in the SDK's core async primitive.

The old project was single-owner; the new one is shared with the team.

On treating critical test infrastructure like production — removing the bus factor.

Endnote

The work is measured in crashes that didn't happen and escalations that never came back.

— Plate II · Iterable Mobile SDK