← Back to DevBytes

tRPC Performance: Optimization Techniques and Benchmarks

Introduction to tRPC Performance

tRPC has revolutionized how developers build full-stack TypeScript applications by providing end-to-end type safety without the need for code generation or schemas. However, as your application scales and traffic increases, the performance of your API layer becomes critical. Understanding how tRPC handles requests under the hood and knowing how to optimize it can mean the difference between a lightning-fast application and a sluggish user experience.

What is tRPC and Why Does Performance Matter?

tRPC is a TypeScript RPC (Remote Procedure Call) framework that allows you to easily build and consume fully type-safe APIs. Because it relies directly on TypeScript's type inference, it eliminates the boilerplate typically associated with REST or GraphQL. Performance matters in tRPC just as it does in any API architecture. While tRPC is inherently lightweight, inefficient procedures, over-fetching of data, and poorly optimized middleware can introduce latency. Optimizing tRPC ensures lower server CPU usage, reduced memory footprint, and faster response times for the end user.

Understanding tRPC Benchmarks

When evaluating tRPC performance, it is helpful to understand how it compares to other API paradigms. Benchmarks consistently show that tRPC introduces minimal overhead compared to raw HTTP handlers, and it generally outperforms GraphQL due to the absence of a complex query parsing and resolution engine. However, tRPC does serialize and deserialize data using "SuperJSON" by default to preserve type fidelity (like Date objects). This serialization step, while incredibly useful, does consume CPU cycles.

Baseline Performance Metrics

To establish a baseline, developers often use tools like autocannon or k6 to stress-test their tRPC endpoints. A standard tRPC procedure executing a simple arithmetic operation can handle tens of thousands of requests per second on a single Node.js instance. The performance drop usually occurs when database queries, authentication, and complex business logic are introduced. Therefore, optimizing tRPC is less about the framework itself and more about how you structure your procedures and data fetching.

Optimization Techniques

1. Leveraging Request Batching

One of the most powerful features of tRPC is automatic request batching. When a client makes multiple tRPC calls in the same render cycle (or tick), the client automatically batches them into a single HTTP request. This drastically reduces network latency and HTTP overhead.

To ensure batching works correctly, you must configure the httpBatchLink on the client side:

import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/router';

export const trpc = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/trpc',
      // You can adjust the maximum number of requests in a single batch
      maxURLLength: 2083, 
    }),
  ],
});

On the client, simply calling multiple procedures synchronously will trigger the batch:

// These two calls will be combined into a single HTTP request
const [user, posts] = await Promise.all([
  trpc.user.getById.query({ id: '1' }),
  trpc.post.getByUserId.query({ userId: '1' }),
]);

2. Preventing Over-Fetching with Database Selectors

A common performance pitfall is fetching entire database rows when only a few fields are needed. When using an ORM like Prisma with tRPC, always use the select or include options to limit the data retrieved from the database. This reduces database load, memory usage, and the time spent serializing the response.

import { z } from 'zod';
import { router, publicProcedure } from '../trpc';

export const userRouter = router({
  getProfile: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      const user = await ctx.prisma.user.findUnique({
        where: { id: input.id },
        // Only select the fields necessary for the profile view
        select: {
          id: true,
          name: true,
          email: true,
          avatarUrl: true,
          // Omitting passwordHash, createdAt, updatedAt, etc.
        },
      });
      return user;
    }),
});

3. Implementing Caching Strategies

For data that changes infrequently, querying the database on every request is wasteful. You can implement caching directly within your tRPC procedures. Using a simple in-memory cache like lru-cache or a distributed cache like Redis can yield massive performance gains.

import { router, publicProcedure } from '../trpc';
import LRUCache from 'lru-cache';

const cache = new LRUCache<string, any>({
  max: 100, // Maximum 100 items
  ttl: 1000 * 60 * 5, // 5 minutes
});

export const productRouter = router({
  getFeatured: publicProcedure.query(async ({ ctx }) => {
    const cacheKey = 'featured-products';
    
    const cached = cache.get(cacheKey);
    if (cached) {
      return cached;
    }

    const products = await ctx.prisma.product.findMany({
      where: { isFeatured: true },
      take: 10,
    });

    cache.set(cacheKey, products);
    return products;
  }),
});

4. Optimizing Middleware

tRPC middleware runs on every request that passes through it. If your authentication or logging middleware performs heavy synchronous operations or makes unnecessary database calls, it will bottleneck your entire application. Ensure middleware is lean and relies on fast lookups (like verifying a JWT token) rather than querying the database on every single request.

import { TRPCError } from '@trpc/server';
import { jwtVerify } from 'jose';

export const isAuthed = t.middleware(async (opts) => {
  const { ctx } = opts;
  
  if (!ctx.token) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }

  try {
    // Fast, stateless JWT verification
    const { payload } = await jwtVerify(ctx.token, getJwtSecret());
    return opts.next({
      ctx: {
        ...ctx,
        user: payload,
      },
    });
  } catch (err) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
});

Best Practices for High-Performance tRPC Apps

Conclusion

tRPC provides an excellent balance of developer experience and performance out of the box. By understanding the underlying mechanics of request batching, serialization, and middleware execution, you can ensure your application remains highly responsive. Implementing database-level optimizations, caching strategies, and lean middleware will allow your tRPC APIs to scale effortlessly, handling high throughput while maintaining the end-to-end type safety that makes tRPC so powerful.

— Ad —

Google AdSense will appear here after approval

← Back to all articles