Skip to content

perf(insights): serve cached insight results as raw JSON without parse round trip - #71268

Merged
thmsobrmlr merged 2 commits into
masterfrom
posthog-code/serve-cached-insight-results-raw-json
Jul 16, 2026
Merged

perf(insights): serve cached insight results as raw JSON without parse round trip#71268
thmsobrmlr merged 2 commits into
masterfrom
posthog-code/serve-cached-insight-results-raw-json

Conversation

@thmsobrmlr

@thmsobrmlr thmsobrmlr commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

TL;DR (eli5)

Opening a big dashboard was slow even when every chart was already cached. The server pulled each cached chart out of Redis, unpacked its JSON into Python objects, checked them, then packed them back into JSON to send — three passes for zero change in the output. Now cached results are stored so the server can grab the already-JSON chunk and drop it straight into the response, skipping the unpack/repack. Same bytes out, much faster on large dashboards.

Problem

On large cached dashboard/insight GETs, most wall time is spent outside the database. Cached query responses were stored as a single JSON blob, so every hit paid orjson.loads on the full results payload, pydantic re-validation, and a re-serialization at render time — the dominant CPU cost when the results are big.

This is the last of three independent, behavior-preserving optimizations extracted from #70967. The first two already landed:

Changes

  • Store cached responses in a split format: magic + flags + header JSON + results JSON. The results field lives in its own segment so a cache hit can skip parsing it. Blobs without the magic prefix are the legacy single-JSON format and stay readable.
  • On a cache hit, when the caller opts in, validate only the small header and embed the raw results segment into the response as an orjson.Fragment — no parse → validate → re-serialize round trip.
  • The raw path is gated to orjson renderers (SafeJSONRenderer / the SSE tile stream) and falls back to the parsed path for: ?indent= pretty-printing, the shared-page template (stdlib json.dumps), run_insights LLM formatting, model-typed result classes, and any query/results carrying custom series names (those need patched parsed results).
  • Split-format writes are always on (for list-shaped results; anything else keeps the legacy single-JSON format). Pods that predate this change treat split entries as cache misses and recompute, so entries written during the brief rolling-deploy window may be recomputed once — an accepted tradeoff since deploys are quick. There is no rollout flag.

How did you test this code?

Automated tests carried over from the source PR:

  • posthog/caching/test/test_query_cache_format.py (new): asserts writes round-trip through the split format by default, non-list results fall back to the legacy format, and pre-existing legacy blobs stay readable.
  • posthog/caching/test/test_calculate_results.py: adds a case proving raw cached results surface as orjson.Fragment and serialize to the expected bytes.
  • products/product_analytics/backend/api/test/test_insight.py: updated mock assertions for the new allow_raw_results kwarg.

In this environment the full Django test suite can't run (no app dependencies installed), so these run in CI. Locally verified: py_compile and ruff check pass on all changed files, and the split encoder/decoder round-trips (legacy blobs, custom-name flag, header-only fallback).

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Extracted the third optimization from #70967 by PostHog Code. Since the first two commits of that PR already merged (#71051, #71022) and the source branch is stale relative to master, the change was isolated by diffing the source PR head against current master per file — dropping the model-typed-results fix and test_calculate_results.py scaffolding that already shipped with #71051, and keeping only the split-cache-format + raw-fragment pass-through delta. Reviewed each file diff to confirm no unrelated master content was reverted.


Created with PostHog Code

…e round trip

Cached query responses were stored as one JSON blob, so every dashboard/insight
GET paid orjson.loads on the full results payload, pydantic re-validation, and a
re-serialization at render time — the dominant CPU cost on large cached dashboards.

Store cached responses in a split format (header JSON + results JSON segments,
legacy blobs stay readable) and, when the insight serializer opts in, validate
only the header and pass the results segment through to the response as an
orjson.Fragment. The raw path is gated to orjson renderers (SafeJSONRenderer /
the SSE tile stream), skipped when pretty-printing, when the shared-page template
needs parsed data (stdlib json.dumps), when run_insights formats results for LLM
output, when the response class types results as models, and when either the query
or the cached results carry custom series names (which require patching parsed
results).

Split-format writes are gated behind QUERY_CACHE_SPLIT_FORMAT_WRITES (default off)
so every deploy ships split-capable readers before any pod writes the new format;
old readers treat split entries as cache misses and recompute.

Extracted from #70967 (final of three independent optimizations; the other two
landed in #71051 and #71022).

Generated-By: PostHog Code
Task-Id: 09583179-7dff-4548-8303-953631e1be33
@trunk-io

trunk-io Bot commented Jul 15, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@thmsobrmlr
thmsobrmlr marked this pull request as ready for review July 16, 2026 05:21
@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested review from a team July 16, 2026 05:21
@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "perf(insights): serve cached insight res..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Playwright — all passed

All tests passed.

View test results →

⚠️ Backend coverage — 92.0% of changed backend lines covered — 12 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ██████████████████░░ 92.0% (148 / 160)

File Patch Uncovered changed lines
posthog/hogql_queries/query_cache_base.py 33.3% 112–115
products/dashboards/backend/api/dashboard.py 50.0% 2347
posthog/caching/fetch_from_cache.py 90.7% 46, 56, 126–128
posthog/hogql_queries/query_runner.py 92.3% 1424, 1522

🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 29495315747 -n patch-coverage), or the coverage-data block at the end of this comment.

Per-product line coverage (touched products)
Product Coverage Lines
platform_features ██░░░░░░░░░░░░░░░░░░ 12.1% 7 / 58
batch_exports ████████░░░░░░░░░░░░ 39.7% 8,414 / 21,220
demo ███████████░░░░░░░░░ 56.2% 1,497 / 2,663
warehouse_sources_queue ████████████░░░░░░░░ 59.2% 148 / 250
tasks █████████████░░░░░░░ 67.4% 25,805 / 38,278
data_tools ██████████████░░░░░░ 70.0% 63 / 90
ai_gateway ███████████████░░░░░ 75.0% 9 / 12
signals ████████████████░░░░ 79.1% 19,033 / 24,073
data_modeling ████████████████░░░░ 80.0% 4,834 / 6,045
cdp ████████████████░░░░ 80.7% 3,118 / 3,864
wizard ████████████████░░░░ 82.5% 772 / 936
notebooks █████████████████░░░ 84.3% 6,343 / 7,520
agent_platform █████████████████░░░ 84.7% 3,273 / 3,862
cohorts █████████████████░░░ 86.1% 4,022 / 4,671
actions █████████████████░░░ 86.6% 717 / 828
product_tours █████████████████░░░ 87.5% 1,266 / 1,447
exports ██████████████████░░ 88.3% 6,891 / 7,800
visual_review ██████████████████░░ 88.5% 5,565 / 6,287
business_knowledge ██████████████████░░ 88.5% 4,400 / 4,969
conversations ██████████████████░░ 88.9% 16,129 / 18,133
mcp_analytics ██████████████████░░ 89.1% 2,502 / 2,807
dashboards ██████████████████░░ 89.2% 5,726 / 6,419
error_tracking ██████████████████░░ 89.6% 9,738 / 10,872
alerts ██████████████████░░ 89.9% 3,638 / 4,046
engineering_analytics ██████████████████░░ 90.1% 5,105 / 5,665
streamlit_apps ██████████████████░░ 90.4% 2,499 / 2,764
slack_app ██████████████████░░ 90.6% 9,511 / 10,503
links ██████████████████░░ 90.6% 183 / 202
marketing_analytics ██████████████████░░ 90.8% 11,514 / 12,684
product_analytics ██████████████████░░ 91.2% 5,606 / 6,150
mcp_store ██████████████████░░ 91.8% 3,668 / 3,995
managed_migrations ██████████████████░░ 91.9% 908 / 988
data_warehouse ██████████████████░░ 92.1% 18,133 / 19,683
workflows ██████████████████░░ 92.4% 5,148 / 5,574
notifications ███████████████████░ 92.7% 1,026 / 1,107
web_analytics ███████████████████░ 92.7% 13,624 / 14,691
ai_observability ███████████████████░ 92.8% 14,868 / 16,019
surveys ███████████████████░ 92.9% 5,687 / 6,120
posthog_ai ███████████████████░ 93.2% 1,322 / 1,418
tracing ███████████████████░ 93.3% 2,435 / 2,611
approvals ███████████████████░ 93.3% 3,395 / 3,640
reminders ███████████████████░ 93.4% 468 / 501
early_access_features ███████████████████░ 93.8% 848 / 904
legal_documents ███████████████████░ 94.1% 1,568 / 1,667
endpoints ███████████████████░ 94.1% 8,606 / 9,143
skills ███████████████████░ 94.4% 2,827 / 2,995
revenue_analytics ███████████████████░ 94.5% 3,598 / 3,809
messaging ███████████████████░ 94.5% 2,530 / 2,677
review_hog ███████████████████░ 94.5% 6,557 / 6,935
logs ███████████████████░ 95.3% 9,528 / 9,994
experiments ███████████████████░ 95.6% 24,234 / 25,351
replay_vision ███████████████████░ 95.7% 13,354 / 13,952
growth ███████████████████░ 95.8% 2,837 / 2,960
annotations ███████████████████░ 96.2% 732 / 761
feature_flags ███████████████████░ 96.3% 16,118 / 16,741
warehouse_sources ███████████████████░ 96.3% 239,624 / 248,804
user_interviews ███████████████████░ 96.4% 2,242 / 2,325
access_control ███████████████████░ 96.8% 849 / 877
data_catalog ███████████████████░ 97.1% 2,034 / 2,095
customer_analytics ███████████████████░ 97.2% 7,480 / 7,698
analytics_platform ████████████████████ 98.0% 2,102 / 2,145
metrics ████████████████████ 98.2% 2,404 / 2,448
live_debugger ████████████████████ 99.2% 613 / 618
field_notes ████████████████████ 99.4% 158 / 159

Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.

@thmsobrmlr
thmsobrmlr removed request for a team July 16, 2026 05:40
@thmsobrmlr thmsobrmlr added the stamphog Request AI approval (no full review) label Jul 16, 2026
@stamphog

stamphog Bot commented Jul 16, 2026

Copy link
Copy Markdown

Note

🤖 stamphog reviewed c582d36fcd4783b51958ad071cf1508d56551213 — verdict: ESCALATE

A cross-team, unreviewed change to the core query-cache serialization/read path used for every dashboard and insight load — mistakes here risk silently corrupted or malformed results in production. It's carefully feature-flag-gated and tested, but that's the author's own engineering, not independent assurance, and familiarity is only MODERATE (not STRONG) with no team-owner review.

  • Author wrote 45% of the modified lines and has 149 merged PRs in these paths (familiarity MODERATE).
  • 👍 on the PR from greptile-apps[bot], hex-security-app[bot].
  • Cross-team change: 5 of 13 files belong to @PostHog/team-analytics-platform (query_cache.py, query_cache_base.py, query_runner.py, calculate_results.py, fetch_from_cache.py) while the author is on @PostHog/team-product-analytics, with no review from the owning team
  • No human or agent review with substantive content is present — only bot reactions (👍 from greptile-apps/hex-security-app) and a self-referential Greptile status comment, which is not the same as a posted review with no unresolved concerns
  • Change reworks the shared query-cache read/write format and raw-results pass-through used across dashboards, insights, sharing pages, and LLM formatting — a subtle bug in the split-format decode or the raw-fragment gating conditions (custom names, model-typed results, indent, sharing) could silently serve wrong or malformed cached data in production
Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list no deny categories matched
size 355L, 10F substantive, 416L/13F incl. docs/generated/snapshots — within ceiling
tier T1-agent / T1d-complex (416L, 13F, two-areas, perf)
stamphog 2.0.0b3 .stamphog/policy.yml @ c9ed54b · reviewed head c582d36

@stamphog stamphog Bot removed the stamphog Request AI approval (no full review) label Jul 16, 2026
Drop the QUERY_CACHE_SPLIT_FORMAT_WRITES rollout gate: split-format cache writes are now always on for list results. Pods that predate the format treat split entries as cache misses during the brief rolling-deploy window and recompute once — an accepted tradeoff since deploys are quick.

Generated-By: PostHog Code
Task-Id: 53eac902-6054-4508-b0f5-a322309561fd
@thmsobrmlr
thmsobrmlr requested a review from a team July 16, 2026 12:26
@thmsobrmlr
thmsobrmlr merged commit f47efb0 into master Jul 16, 2026
262 checks passed
@thmsobrmlr
thmsobrmlr deleted the posthog-code/serve-cached-insight-results-raw-json branch July 16, 2026 13:33
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 16, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-16 14:03 UTC Run
prod-us ✅ Deployed 2026-07-16 14:27 UTC Run
prod-eu ✅ Deployed 2026-07-16 14:26 UTC Run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants