Skip to content

feat(recommendations): bound GET /api/recommendations with pagination and slimmer rows #777

Open

Description

@cristim

Summary

PR for #723 gzips large JSON responses on Lambda, which raises the 6 MB ceiling several-fold but does not bound GET /api/recommendations. Row count still grows with the tenant.

Current behaviour

getRecommendations (internal/api/handler_recommendations.go) returns the full filtered cache; the store read (store_postgres_recommendations.go) has no ORDER BY, LIMIT or cursor. Scoping showed details is only about a quarter of a row (roughly 125 B of 500-600 B), so dropping it alone saves little.

Constraints found while scoping

  • frontend/src/dashboard.ts and frontend/src/recommendations.ts compute totals, grouping and sorting client-side over the full set; server pagination would need server-side summaries and a frontend change.
  • samePurchaseVariantIdentity (frontend/src/recommendations.ts) reads details fields from list rows to group priced variants; dropping details needs top-level discriminators or an on-demand fetch.
  • GET /api/recommendations/{id}/detail does not return the details blob.

Proposed fix

Keyset pagination with server-side summary, slimmer list rows, and documenting min_savings_* in openapi.yaml. Needs a design pass with the frontend owners.

References

Parent #723. Found by the 2026-10-08 review fleet (COR-04).

Activity

  1. IAmJSD commented on Oct 26, 2026

    @IAmJSD
    Collaborator

    Claiming: working on this

  2. IAmJSD commented on Oct 26, 2026

    @IAmJSD
    Collaborator

    Scoping update. Nothing is coded yet: the issue asks for a design pass with the frontend owners, and the remaining choices are product decisions for the acting owner, not code.

    Where things stand on main

    • fix(api): add opt-in in-memory cursor pagination to GET /api/recommendations #974 (merged) added opt-in limit/cursor paging to GET /api/recommendations. It pages in memory, caps each page at 4 MiB, and still computes summary/regions over the full filtered set. A request with neither param gets the old unbounded body.
    • perf(api): page recommendations from an ID-ordered keyset scan instead of the full list #1240 (draft, ours) moves the paging into a SQL keyset scan. It needs a migration (000142), and that number is caught up in the renumbering, so it is parked.
    • The frontend (frontend/src/api/recommendations.ts getRecommendations, called from dashboard.ts and recommendations.ts) never sends limit. It still takes the unbounded path, so a large tenant can still hit the 6 MB Lambda cap when the client doesn't accept gzip.
    • samePurchaseVariantIdentity (recommendations.ts ~1090) still reads details from list rows, so details can't be dropped from list rows yet.

    Options

    1. Frontend follows the cursor (small). getRecommendations sends limit=1000 and follows next_cursor until it is absent, concatenating the rows. Client-side grouping, sorting and totals stay as they are, and no response can go over the cap. Cost: N rows take ceil(N/1000) requests, and each one re-reads the whole filtered set on the server (O(P*N) today, still O(N) per page after perf(api): page recommendations from an ID-ordered keyset scan instead of the full list #1240), so a large tenant's load gets slower even though it no longer fails. Client memory is unchanged.
    2. True server-side paging (large). The server sorts, groups and sums, and the frontend renders one page at a time, using summary for the totals. This bounds both the response and client memory. It needs server-side equivalents of the dashboard and recommendations grouping/sorting, and a redesign of the recommendations table interaction (sorting and grouping across pages, bulk select across pages).
    3. Slimmer rows (medium, independent). Promote the fields samePurchaseVariantIdentity reads from details to top-level row fields (as already done for availability_zone), drop details from list rows, and serve it from GET /api/recommendations/{id}/detail. On its own this saves about a quarter of each row.

    Recommendation: do option 1 now as the fix for the 502, since it is low risk and needs no design. Unpark #1240 once migration numbers settle, so each page stops re-sorting the whole set. Keep options 2 and 3 open until the recommendations UI rework is planned, because they mostly depend on how that table should behave.

    Released: needs an owner decision between the options above; no code written

  3. IAmJSD commented on Oct 26, 2026

    @IAmJSD
    Collaborator

    Released: needs an owner decision between the options in the comment above; no code written

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions