How I parsed billions of rows for every user in 2 seconds
Based on Theo - t3․gg's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.
Convex handled app workflows well, but it wasn’t suitable for per-user analytical scans across massive event tables.
Briefing
A wrapped-style analytics feature that once took 10–20 minutes per user was brought down to under 10 seconds—and in some cases to a few hundred milliseconds—by changing how usage statistics are computed and cached. The core problem wasn’t the UI or even the analytics logic; it was the data plumbing. Convex powered the app backend, but it wasn’t built for scanning and aggregating billions of analytics rows per request. Post Hog was better suited for analytics, yet its ClickHouse-based query model and rate limits made repeated, heavy queries too slow and too error-prone when multiplied across thousands of users.
The initial architecture ran a Convex workflow that queued a “compute wrapped” job and then executed multiple Post Hog queries to assemble the stats shown in the wrapped experience: model usage counts, feature usage (edits, branches, retries), and time breakdowns (by day, weekday, and hour). Several steps required full scans and grouping—especially percentiles, which had to compare each user’s total usage against the distribution across all users. Early timing showed the first query step in hundreds of milliseconds, but model usage and feature usage ballooned into tens of seconds, and percentile calculations reached roughly 30 seconds. Worse, Post Hog rate limits (with concurrency and per-minute/hour caps) turned the situation into a cascading failure: the system attempted about 10 queries per user, and the queue quickly accumulated thousands of long-running generations.
The turnaround came from two tactics: reducing query pressure and shifting expensive computation into precomputed caches. First, the team added operational controls (feature flags, higher parallelism limits within reason, more logging, and fixing a default Post Hog date limit that capped results at 100 days). Then they rewrote queries to reduce the number of separate requests—combining logic into fewer SQL statements using unions to stay under rate limits.
The biggest leap, however, came from materialized views inside Post Hog. Instead of recomputing “usage by user” and “model usage by user” from raw event tables every time, the system precomputed subqueries into cached tables (effectively stored results that could be queried like normal tables). Once those materialized views existed, the wrapped queries became dramatically simpler: percentile calculations could be expressed as counts over cached per-user aggregates rather than repeated full-table scans. That change collapsed end-to-end runtimes from 6+ minutes to under a second for key steps, and production generations began completing in roughly 10 seconds on average.
From there, further refinements squeezed the remaining overhead: collapsing multiple actions into fewer Convex workflow steps, using Post Hog Endpoints (a beta feature) to expose dashboard-defined queries over HTTP for better caching behavior, and experimenting with “single-shot” query shapes that compute more at once. After these iterations, reruns sometimes landed under 200 milliseconds due to warm caching.
The practical takeaway is that analytics at wrapped scale demands precomputation and careful respect for query limits. The performance win wasn’t magic SQL—it was architectural alignment: compute once, cache aggressively, and design queries to avoid repeated scans of massive event histories.
Cornell Notes
The wrapped feature’s runtime collapsed from 10–20 minutes (and sometimes hours under load) to under 10 seconds by changing how analytics are computed. The original approach issued many Post Hog queries per user, including full-table scans and percentile calculations that triggered Post Hog rate limits and timeouts. The breakthrough was building Post Hog materialized views—cached per-user aggregates like “usage by user” and “model usage by user”—so later wrapped requests could query small precomputed tables instead of scanning billions of rows. Additional gains came from query consolidation (fewer requests via unions), action/workflow restructuring in Convex, and using Post Hog Endpoints to benefit from endpoint caching. The result: near-instant wrapped generation for many users and far fewer production failures.
Why did the first implementation take so long even though it used an analytics database?
What specific Post Hog rate-limit problem made the situation worse?
How did materialized views change the performance profile?
What did query consolidation (unioning) accomplish?
Why did using Post Hog Endpoints help after materialized views?
Why did the team avoid “live” time windows for the cached analytics?
Review Questions
- What makes percentile calculations particularly expensive in this wrapped analytics setup, and how did the materialized-view approach address that cost?
- How do Post Hog rate limits interact with a workflow that issues many queries per user, and what strategies reduced the impact?
- Describe the sequence of optimizations from “many full scans” to “cached aggregates.” Which change produced the largest runtime drop?
Key Points
- 1
Convex handled app workflows well, but it wasn’t suitable for per-user analytical scans across massive event tables.
- 2
On-the-fly percentiles and other grouped aggregations triggered full-table scans and pushed runtimes into tens of seconds per step.
- 3
Post Hog rate limits (concurrency plus per-minute/per-hour caps) turned heavy per-user query patterns into timeouts and queue backlogs.
- 4
Materialized views were the main breakthrough: precompute per-user aggregates once, then compute wrapped stats by querying cached tables instead of raw events.
- 5
Reducing the number of Post Hog requests (e.g., unioning logic into fewer queries) improved reliability by staying under rate limits.
- 6
Further speedups came from restructuring Convex workflow actions and using Post Hog Endpoints to benefit from endpoint-level caching.
- 7
Treating the analytics window as effectively static made caching practical and avoided cache-staleness problems from new incoming events.