Server Actions vs tRPC in 2026: When to Use Which?
- Server Actions: zero-boilerplate, structured-clone serialization, React 19 hooks
- tRPC v11: end-to-end types, batching, caching via TanStack Query
- Hybrid pattern is production default in 2026
- 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
Production Incident
Production Debug GuideCommon failures and how to diagnose them
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.
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.
'use server'; import { revalidateTag } from 'next/cache'; import { db } from '@/lib/db'; import { z } from 'zod'; const UpdateStockSchema = z.object({ productId: z.string().uuid(), quantity: z.number().int().min(0) }); export async function updateStock(prevState: any, formData: FormData) { const parsed = UpdateStockSchema.safeParse({ productId: formData.get('productId'), quantity: Number(formData.get('quantity')) }); if (!parsed.success) return { error: parsed.error.flatten() }; await db.inventory.update({ where: { productId: parsed.data.productId }, data: { quantity: parsed.data.quantity, updatedAt: new Date() }}); revalidateTag('inventory'); return { success: true }; }
- 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().
import { z } from 'zod'; import { createTRPCRouter, protectedProcedure } from '../trpc'; import { TRPCError } from '@trpc/server'; export const inventoryRouter = createTRPCRouter({ getByProduct: protectedProcedure.input(z.object({ productId: z.string().uuid() })).query(async ({ ctx, input }) => { const inv = await ctx.db.inventory.findUnique({ where: { productId: input.productId }}); if (!inv) throw new TRPCError({ code: 'NOT_FOUND' }); return inv; }), updateStock: protectedProcedure.input(z.object({ productId: z.string().uuid(), quantity: z.number().int().min(0) })).mutation(async ({ ctx, input }) => { return ctx.db.inventory.update({ where: { productId: input.productId }, data: { quantity: input.quantity, updatedAt: new Date() }}); }) });
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.
'use client'; import { api } from '@/trpc/react'; import { updateStock } from '@/app/actions/inventory'; import { useActionState, useOptimistic } from 'react'; export default function Dashboard() { const { data: lowStock } = api.inventory.listLowStock.useQuery({ threshold: 10 }); const [optimistic, addOptimistic] = useOptimistic(lowStock, (s, u) => s.map(i => i.productId === u.id ? { ...i, quantity: u.qty } : i)); const [state, formAction] = useActionState(updateStock, null); return ( <ul> {optimistic?.map(item => ( <li key={item.productId}> {item.product.name} - {item.quantity} <form action={async (fd) => { addOptimistic({ id: item.productId, qty: Number(fd.get('quantity')) }); await formAction(fd); }}> <input type='hidden' name='productId' value={item.productId} /> <input type='number' name='quantity' defaultValue={item.quantity} /> <button>Update</button> </form> </li> ))} </ul> ); }
- 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.
export const BENCHMARKS = { singleMutation: { serverAction: '45ms', trpc: '50ms', delta: '5ms' }, eightQueries: { withoutBatch: '496ms', withBatch: '68ms', improvement: '7.3x' }, cached: { first: '120ms', cached: '0ms', serverAction: '120ms' } };
| Feature | Server Actions | tRPC |
|---|---|---|
| Setup complexity | Zero β 'use server' | Moderate β router, client |
| Type safety | Inferred from import | Automatic end-to-end |
| Input validation | Manual zod | Built-in zod |
| Query caching | None | TanStack Query |
| Request batching | No | httpBatchLink |
| Optimistic updates | Manual via useOptimistic (React 19) | Built-in via onMutate |
| Middleware | None β wrap manually | Full chain |
| React form integration | Native β useActionState + useFormStatus | Manual useMutation |
| Serialization | Structured clone (Date/Map/Set/BigInt work) | JSON; add superjson for Date/Map |
| Error handling | Throw Error or return state | TRPCError with codes |
| Subscriptions | No | Yes via wsLink |
| Best for | Simple mutations <20 endpoints | Complex queries >50 endpoints |
π― Key Takeaways
- Server Actions: zero-boilerplate, structured-clone serialization, React 19 hooks
- tRPC v11: end-to-end types, batching, caching via TanStack Query
- Hybrid pattern is production default in 2026
- Share zod schemas between both
- revalidateTag requires fetch with next tags β do both
β Common Mistakes to Avoid
Interview Questions on This Topic
- QWhen choose Server Actions vs tRPC?Mid-levelReveal
- QBiggest limitation of Server Actions?Mid-levelReveal
- QHow does httpBatchLink work?SeniorReveal
- QHow handle optimistic updates?SeniorReveal
- QCan they coexist?Mid-levelReveal
Frequently Asked Questions
Do Server Actions work with React 19 useFormStatus?
Yes. useFormStatus gives pending state. useActionState manages form state and errors. useOptimistic enables optimistic UI.
Can tRPC work with App Router?
Yes. Create app/api/trpc/[trpc]/route.ts. Use createTRPCReact from @trpc/react-query with httpBatchLink. Server components can use createCaller directly.
Is tRPC still relevant with Server Actions?
Yes. Server Actions lack caching, batching, subscriptions. tRPC fills those gaps. Hybrid is standard, not replacement.
How test both?
Server Actions: call function directly with FormData. tRPC: use createCaller to test procedures without HTTP.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.