Server Actions vs tRPC — Stale UI After Mutation
Server Actions don't auto-revalidate.
- Server Actions are Next.js-native RPC functions ('use server') that compile to internal POST endpoints
- tRPC v11 provides end-to-end type safety with automatic TypeScript inference from router definitions
- Server Actions excel for simple mutations (forms, toggles) — zero boilerplate, native React 19 integration via useActionState and useFormStatus
- tRPC excels for complex queries — batching via httpBatchLink, caching via TanStack Query, subscriptions, middleware chains
- Server Actions serialize via structured clone (Date, Map, Set, BigInt, File work since Next.js 14.2) — not JSON-only
- Both require explicit cache invalidation: Server Actions with revalidateTag/revalidatePath (fetch must use next tags), tRPC with utils.invalidate()
- Use Server Actions for <20 mutations; choose tRPC when you need query caching, batching, or >50 typed endpoints
Imagine ordering food. Server Actions are like texting your order directly to the kitchen — fast, simple, no intermediary. tRPC is like a full restaurant ordering system with a menu (typed routes), a waiter (middleware), a kitchen display (error handling), and the ability to track multiple orders at once (batching). For a quick coffee, texting works fine. For a 20-course tasting menu, you need the system.
Next.js 16 Server Actions and tRPC v11 solve the same problem — moving data between client and server without manual HTTP endpoints — but they make different trade-offs. Server Actions prioritize simplicity: write a function, mark it 'use server', call it from a component with React 19's useActionState. tRPC prioritizes developer experience: define a router, get end-to-end types, automatic batching, and full TanStack Query integration.
The wrong choice doesn't crash — it causes architectural decay. Teams using Server Actions for 50-endpoint features end up with scattered logic and no caching. Teams using tRPC for a simple contact form spend more time on setup than building.
This 2026 guide breaks down exactly when each wins, and the hybrid pattern production teams use: Server Actions for writes, tRPC for reads.
Why Server Actions and tRPC Both Lie About Your UI State
Server Actions (Next.js) and tRPC are two strategies for calling server logic from the client. Server Actions are RPC-like functions embedded in React components that run on the server, using form actions or direct calls. tRPC is a typed RPC framework that generates client-side hooks from a server router, enforcing type safety across the wire. Both eliminate manual fetch/API route wiring, but they differ in how they handle cache invalidation and UI reconciliation after mutations.
Server Actions rely on React's server component model: after a mutation, you typically call revalidatePath() or revalidateTag() to purge the server cache. The client does not automatically know the new state — it must re-fetch the affected data. tRPC, by contrast, uses React Query under the hood: mutations can automatically invalidate specific query keys, and the client refetches stale queries in the background. The key practical difference: Server Actions leave the UI stale until you explicitly revalidate; tRPC can trigger refetches declaratively via query key dependencies.
Use Server Actions when you want tight integration with Next.js server components and prefer explicit cache control. Use tRPC when you need fine-grained, automatic cache invalidation across many queries and mutations, especially in data-heavy dashboards or real-time apps. The choice matters because stale UI after a mutation is the most common production bug in both patterns — and the fix is not in the mutation code, but in how you declare data dependencies.
How Server Actions Work Under the Hood
A Server Action is a function marked 'use server' that Next.js compiles into a server-only module. When called from the client, Next.js creates an encrypted POST to an internal endpoint, deserializes arguments via structured clone, executes the function, and returns the result.
Structured clone (Next.js 14.2+) supports Date, Map, Set, BigInt, RegExp, ArrayBuffer, File, and Blob. Functions, class instances with prototypes, and Symbols still fail. You no longer need to stringify Dates manually.
React 19 adds useActionState for form state and useOptimistic for optimistic UI — Server Actions can now do optimistic updates without manual state management.
- Write function, mark 'use server', call from client — zero boilerplate
- Structured clone serialization — Date/Map/Set/BigInt work; functions/class instances don't
- No middleware — add auth/logging inline or via wrapper
- Cache invalidation manual — revalidateTag AND tag your fetches
- React 19: useActionState for state, useFormStatus for pending, useOptimistic for optimistic UI
How tRPC Works Under the Hood
tRPC v11 defines a router of procedures on the server. The client imports the router type via createTRPCReact from @trpc/react-query, and TypeScript infers input/output types automatically. No manual types.
The client uses links. httpBatchLink batches calls made in the same event loop tick into one HTTP POST. Zod validation runs only on the server — not twice. Add superjson transformer to support Date/Map/Set.
tRPC integrates with TanStack Query v5. Queries are cached, deduplicated, and stale-while-revalidated. Mutations invalidate caches via utils.invalidate().
The Hybrid Architecture: Server Actions for Writes, tRPC for Reads
Production default in 2026: Server Actions for mutations, tRPC for queries. Server Actions give React 19 form integration with useActionState. tRPC gives TanStack Query caching and batching.
Both can do optimistic updates: tRPC via onMutate, Server Actions via useOptimistic. Share zod schemas to prevent drift.
- tRPC handles all reads: cached, batched, background refetch
- Server Actions handle simple writes: forms with useActionState
- Both can do optimistic UI: tRPC via onMutate, Actions via useOptimistic
- Share zod schemas in lib/schemas
Performance Comparison: Real Numbers
Single mutation: Server Actions ~45ms, tRPC ~50ms — ~5ms difference from link overhead, not double validation. 8 queries: without batching 496ms, with httpBatchLink 68ms — 7.3x faster. Cached tRPC query: 0ms vs Server Action always 120ms.
Why Both Architectures Break at 10,000 Concurrent Users — and What You Can Do About It
You've read the performance numbers. You know Server Actions serialize through React's reconciler and tRPC maintains WebSocket pools. But here's the hard truth nobody tells you: both architectures fall apart under real-world concurrent load.
ActionQueue.drain() or switch to tRPC for high-concurrency endpoints.The Missing Fucking Piece: Error Handling Contracts
Every tutorial shows you the happy path. Server Actions return 'success: true'. tRPC gives you typed errors. But real systems have network partitions, database deadlocks, and third-party API timeouts. Neither framework tells you how to handle the cascading failure when your database pool is exhausted.
Why Server Actions and tRPC Both Lie About Your UI State
Every optimistic update is a fucking lie until the server confirms it. Server Actions revalidate on response, tRPC invalidates queries on mutation success — both assume the network cooperates. That assumption breaks when a write succeeds on the server but the client never gets the ack due to timeout, mid-air collision, or your cloud provider having a bad Tuesday.
The real problem isn't the tech. It's that both patterns encourage you to trust your local cache as the source of truth. They aren't. Your database is. Until you enforce a hard reconciliation step — a background sync, a polling fallback, or a state machine that forces revalidation on navigation — you're shipping a UI that gaslights users into thinking their action completed when it didn't.
Production fix: never cache a mutation result without an expiration timer. Treat every optimistic update as provisional until the next full page load.
The Hybrid Architecture: Server Actions for Writes, tRPC for Reads
Stop treating this as an either-or war. The smart play is a hybrid: Server Actions for mutations because they get you progressive enhancement out of the box and handle form state without client JS overhead. tRPC for reads because it gives you type-safe queries, caching, and automatic invalidation that Server Actions can't touch.
The reason this works: writes are eventually consistent by nature — a form submit can afford a 200ms delay. Reads must be fast and fresh. Server Actions for writes mean you get free loading states, error boundaries, and the ability to work without JavaScript enabled. tRPC for reads gives you query deduplication, stale-while-revalidate, and a declarative fetch model that Server Actions lack.
Deploy this: keep your mutation logic in Next.js Server Actions. Expose your data fetching through tRPC routers. Wire the invalidation so that a successful Server Action tells the tRPC client to refetch. You get the best of both without the dogma.
Seeking Suggestions: tRPC vs. Next.js Server Actions for a Next.js Project
When starting a Next.js project, the choice between tRPC and Server Actions comes down to one question: who owns the data flow? Server Actions tie mutations directly to the Next.js request lifecycle — ideal for form submissions and progressive enhancement where you want zero boilerplate for simple create/update/delete. But they hide complexity: you cannot inspect the network payload, error boundaries are implicit, and caching is tightly coupled to the React tree. tRPC, conversely, exposes a typed RPC layer that forces explicit transport — every call is debuggable, every error has a code, and caching is your responsibility (React Query, SWR). In practice: use Server Actions when your app is form-centric and you want maximum simplicity for mutations. Use tRPC when you need a shared API contract across clients (mobile, web, third-party) or when reads require fine-grained cache control. The trap is mixing both without a boundary — you’ll duplicate validation and lose traceability.
I Am Comparing in This Way
Here’s the only comparison framework that matters: input → output → observable side effects. Server Actions and tRPC both accept input, return output, and can trigger side effects — but their contracts differ fundamentally. With tRPC, you get a typed, serializable input/output contract enforced at the client edge — any violation instantly breaks the type chain. Server Actions give you a form-action contract: the input is FormData or a plain object, the output is whatever you return (usually JSON), but the contract is implicit — there’s no type safety on the client unless you manually share types. The performance difference? Negligible for 95% of apps (both are ~1-5ms overhead per call). The real breakpoint is observability: tRPC logs every call with input/output sizes and error codes. Server Actions are hidden inside React’s reconciliation, making debugging a chore. If you need logs and latency breakdowns, pick tRPC. If you want simplicity and form integration, pick Server Actions. Do not pick both on the same route — you lose the epistemic value of a single contract.
Server Actions with no revalidation caused stale UI for 6 hours during a product launch
- Server Actions do NOT auto-revalidate — call revalidateTag or revalidatePath
- revalidateTag only works if fetch is tagged — add next: { tags: [...] } to data fetches
- Test revalidation by checking UI after mutation, not just DB
- During launches, monitor DB vs cache divergence
utils.inventory.invalidate() — TanStack Query serves cached response until invalidated.Key takeaways
Common mistakes to avoid
4 patternsUsing Server Actions for read-heavy dashboards
Forgetting revalidateTag AND fetch tags
Using tRPC for simple 2-form feature
Not sharing zod schemas
Interview Questions on This Topic
When choose Server Actions vs tRPC?
Frequently Asked Questions
That's React.js. Mark it forged?
6 min read · try the examples if you haven't