How We Think About Threads iOS Performance

How the Threads iOS team built a custom metrics framework — %FIRE, TTNC, and cPSR — to measure what users actually feel, and used deliberate latency injection to quantify exactly how much slowness users will tolerate.

How We Think About Threads iOS Performance

Performance is often treated as a single number — a P99 latency or a crash rate — but Threads’ engineering shows that choosing the wrong number is as damaging as not measuring at all. The question is not “how fast is the app?” but “which slowness are users actually abandoning us for?”

Source

How we think about Threads’ iOS performance

By: Meta
Published: Dec 18 2024
Type: Engineering article

Saved for the boundary test methodology alone — deliberately injecting latency into a live app to measure user sensitivity is a technique most teams never attempt, and the data it produced here is unusually clean. The custom metric definitions (%FIRE, TTNC, cPSR) are also worth studying as a template for building signal that reflects user experience rather than infrastructure health.

Measuring What Users Feel, Not What Servers Report

Threads launched in 2023 and reached 100 million users in five days. By the time this article was written, it had grown to over 300 million monthly users and the contributing engineering team had expanded from a small group to more than a hundred people. At that scale, the standard bag of metrics — server response time, CDN cache hit rate, crash rate by build — tells you a lot about infrastructure but surprisingly little about whether users are experiencing the app as fast. The Threads iOS team built three custom metrics that sit closer to user perception.

The first is %FIRE, the percentage of people who experience a frustrating image-render experience. The idea is straightforward: if an image takes too long to appear after a user should reasonably expect it, that event contributes to %FIRE. What makes it useful is that it is computed per-user rather than per-request, which means a single user having ten bad image loads counts once, not ten times — the metric reflects how many people are frustrated, not how many individual loads regressed. The flip side is that it does not catch every image bug. A flickering profile photo (caused, it turned out, by incorrect view model comparison logic during cell reuse) was a real user experience regression that did not move %FIRE at all, because the images were technically loading — just animating incorrectly. You still need qualitative feedback channels to catch that class of bug.

The second is TTNC, time-to-network content. This is where the naming does the most conceptual work. “Time-to-content” is a common metric, but it usually measures how long until something appears on screen — and that something is often cached content from the previous session. Threads is a live commentary feed; showing cached posts from six hours ago quickly is not actually fast from the user’s perspective. TTNC specifically measures time to a fresh, personalized feed, which is a much harder bar to hit. To improve it, the team focused heavily on binary size, because a smaller binary launches faster and compiles faster during development. Threads’ binary ended up roughly one-quarter the size of Instagram’s, which also gives the app a two-times faster non-incremental build time — a developer experience improvement that compounds over hundreds of engineers making dozens of changes per day.

The third is cPSR, creation-publish success rate, tracked separately for text, photos, and video. Posting a photo or video on iOS involves local transcoding, asynchronous upload, and a narrow OS-enforced background execution window. If a user taps “Post” and then backgrounds the app, iOS may give the process only seconds before terminating it. cPSR surfaces exactly how often the full publish pipeline succeeds rather than silently failing — a class of failure that does not show up in server error rates because the request was never made.

The most technically interesting part of the article is the boundary test for navigation latency. Rather than measuring existing latency and guessing what the impact would be if they improved it, the team ran a controlled experiment: they deliberately injected small amounts of latency into profile views, conversation views, and the activity feed for a randomly selected subset of users. They tested three levels of injected latency across both cold-start navigation (launching the app from the OS) and in-app navigation (tapping within a running app). The results showed that iOS users are sensitive to latency even in sub-second ranges. The smallest injection had negligible effects in some views but meaningful ones in others; the largest injections caused measurable drops in daily active users, foreground session time, likes, and conversation views — even though the latency was injected into secondary surfaces, not the core feed. The lesson is that users do not neatly distinguish “slow profile” from “slow app.” Latency anywhere in the navigation graph degrades the overall experience, even surfaces users visit infrequently.

To measure navigation latency consistently across a large, heterogeneous app, the team built the SLATE debugger. Navigation latency is deceptively hard to instrument: you need a consistent “start” event across surfaces that initialize in different ways, a consistent “end” event that means “content has rendered,” and a way to separate cold-start timing from in-app timing — because the two have fundamentally different performance budgets and different bottlenecks. SLATE provides a standard API for teams to attach timing to any navigation path without each surface reimplementing the instrumentation from scratch.

The recurring pattern across all three metrics and both deep dives is that the Threads team consistently refused to borrow generic infrastructure metrics and instead asked: “What would a user describe as slow or broken?” That question produces different instrumentation than “What can our monitoring stack easily measure?” and it produces different priorities as a result.

My take

The boundary test is underused across the industry, and the reason is obvious: it requires intentionally making the app worse for a real subset of users, which most teams treat as an ethical or political blocker rather than an engineering tradeoff. What Threads’ data shows is that without this kind of experiment, you are guessing at user sensitivity. You might optimize a surface that users barely notice and ignore one where a 200ms regression costs you 1.6% of daily active users. The experiment quantifies the asymmetry — some surfaces are four times more sensitive to latency than others — and that asymmetry should directly determine where you allocate engineering headcount. Running the experiment costs a small amount of short-term user experience quality in exchange for permanent accuracy about what actually matters.

Where this is useful

  • Deciding which iOS performance surfaces to prioritize when you have limited engineering capacity
  • Building a custom mobile metrics framework that distinguishes infrastructure health from user-perceived quality
  • Designing navigation latency instrumentation that works consistently across heterogeneous app surfaces
  • Not applicable when your user base is small enough that running a controlled latency experiment would produce statistically insignificant results — below roughly 50,000 DAU, the signal will be too noisy to act on

Apply this: Before optimizing any navigation surface, inject a small amount of latency into a control group to measure actual user sensitivity — then prioritize surfaces by sensitivity, not by how easy they are to optimize.

What I want to remember

Remember: User-perceived performance metrics (%FIRE, TTNC, cPSR) and infrastructure metrics (server latency, cache hit rate) are measuring different things — building the former requires asking what users would describe as broken, not what your monitoring stack can easily instrument.