From 37c2238624b119ab611b9b9ac3a1a53b7c9603df Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Date: Wed, 5 Aug 2026 14:36:32 +0200 Subject: [PATCH] perf: stream the dashboard tRPC batch response (#2549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit httpBatchLink holds every result in a batch until the slowest procedure settles, so a batch mixing cheap queries with a Tinybird pipe delivers nothing for seconds. Production P75 shows the same shell procedures at 16ms alone and 2.26s when batched alongside Tinybird. httpBatchStreamLink flushes each procedure as it resolves. Total request duration is unchanged — the request still ends with the slowest op — so the metric to watch is TTFB (P75 1.41s on this route), not Duration. Passing AppRouter as the type argument types the options directly, which removes the satisfies Partial> workaround and its FIXME rather than relocating them. Tradeoff: headers flush before any procedure resolves, so per-procedure errors now arrive in-band on a 200 and this route's 4XX band disappears from Vercel. Sentry coverage is unaffected — createTRPCOnError and sentryLoggerLink both still fire — and timingMiddleware already logs per-procedure ok, which is finer-grained than a batch-level status. Co-authored-by: Claude Opus 5 (1M context) --- apps/dashboard/src/lib/trpc/shared.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/dashboard/src/lib/trpc/shared.ts b/apps/dashboard/src/lib/trpc/shared.ts index 110c9eb5..01f8f361 100644 --- a/apps/dashboard/src/lib/trpc/shared.ts +++ b/apps/dashboard/src/lib/trpc/shared.ts @@ -1,7 +1,7 @@ import type { AppRouter } from "@openstatus/api"; import * as Sentry from "@sentry/nextjs"; -import type { HTTPBatchLinkOptions, HTTPHeaders, TRPCLink } from "@trpc/client"; -import { httpBatchLink, loggerLink } from "@trpc/client"; +import type { HTTPHeaders, TRPCLink } from "@trpc/client"; +import { httpBatchStreamLink, loggerLink } from "@trpc/client"; import superjson from "superjson"; /** @@ -56,16 +56,19 @@ const getBaseUrl = () => { // The whole tRPC surface is served from a single Node.js endpoint — there is // no longer an Edge/Node split, so all calls go to one link. +// Streaming rather than plain batching: a batch mixing cheap queries with a +// slow Tinybird pipe would otherwise deliver none of them until the slowest +// one settles. Note this makes per-procedure errors arrive in-band on a 200 — +// `sentryLoggerLink` above is what still reports them. export const endingLink = (opts?: { fetch?: typeof fetch; headers?: HTTPHeaders | (() => HTTPHeaders | Promise); }): TRPCLink => (runtime) => - httpBatchLink({ + httpBatchStreamLink({ headers: opts?.headers, fetch: opts?.fetch, transformer: superjson, url: `${getBaseUrl()}/api/trpc/lambda`, - // oxlint-disable-next-line typescript/no-explicit-any -- FIXME: remove any - } satisfies Partial>)(runtime); + })(runtime); -- 2.51.2