Skip to Content
Route Performance Overhead

Next.js API Route Performance Overhead

Summary

  • Goal: measure Vovk.ts overhead over native Next.js route handlers (not the HTTP stack).
  • Routing: ~1.5 µs median per request from 1 to 1,000 controllers, ~1.75–1.83 µs at 10,000 (20,000 endpoints). Throughput ~545k–675k ops/s per core. The test repeats one path, which the route cache serves; see the notes below the results.
  • Cold start: grows linearly. ~8 ms at 1,000 controllers, ~114 ms at 10,000. About 11–12× the cost of no-op decorators.
  • Notes: Tinybench on Node.js 24.1 and an Apple M4 Pro, measured on 2026-10-06. Next.js runtime cost is not measured. Written from real benchmark output with AI help and small edits. The scripts are in the perf folder of the Vovk.ts repository.

Reproducing the Tests

Clone the repository, install dependencies at its root, and build the packages:

git clone https://github.com/finom/vovk.git cd vovk npm ci npm run build

Run the performance tests from the perf folder. The script first generates the 11,111 test controllers.

cd perf npm run perf-test

The suite also runs in CI: GitHub Actions . CI uses GitHub-hosted runners, which are slower than a local M4 Pro, so its numbers are higher; the scaling is the same.

Overview

Vovk.ts runs on top of Next.js API routes and builds the handlers from decorated procedures:

src/app/api/[[...vovk]]/route.ts
export const { GET, POST } = initSegment({ controllers });

The tests measure the overhead in two ways:

  • Request overhead: routing and handler time per request.
  • Cold start overhead: time to initialize controllers and metadata.

Request Overhead

Example controller (N = 1) used in the request-overhead tests:

src/modules/one/a/a-controller.ts
import { procedure, prefix, get, post, operation } from "vovk"; import z from "zod"; @prefix("a") export default class AController { @operation({ summary: "Get A", }) @get() static getA = procedure().handle(() => { return { get: true }; }); @operation({ summary: "Create A", }) @post("{id}") static createA = procedure({ disableServerSideValidation: ["params"], params: z.object({ id: z.string() }), }).handle((_req, { id }) => { return { post: true, id }; }); }

Every test controller is generated by scripts/createModules.ts.

Methodology (short)

  • Generate N controllers (N ∈ {1, 10, 100, 1,000, 10,000}), each with:
    • GET without params.
    • POST with path param “{id}” (pattern match).
  • Minimal handlers; the full routing and handler path is measured.
  • Tinybench: at least 100 ms per test, nanosecond timing; median latency and throughput.

Results

ControllersEndpointsGET Latency (med)POST Latency (med)GET Throughput (med ops/s)POST Throughput (med ops/s)
121,500 ns1,500 ns666,667666,667
10201,500 ns1,480 ns666,667676,034
1002001,500 ns1,500 ns666,667666,667
1,0002,0001,500 ns1,500 ns666,667666,667
10,00020,0001,833 ns1,750 ns545,554571,429

Key takeaways:

  • Flat latency up to 1,000 controllers, about 20% higher at 10,000.
  • ≈1.5 µs overhead at typical sizes; GET ≈ POST, so reading the path param costs little.
  • The test repeats one path, which the route cache serves. The first request to a new path with a param scans the routes of its method: about 2.7 µs at 10 controllers, 42 µs at 1,000 and 0.8 ms at 10,000. A path that matches no route is never cached and costs about 3.4 µs, 123 µs and 3.1 ms.

Cold Start Overhead

Example cold-start benchmark (N = 1), Vovk.ts against no-op decorators:

bench/generated_coldStartPerfTest.ts
bench.add("Cold start for 1 controllers", async () => { const controllers: Record<string, Function> = {}; @prefix("one/0") class One0Controller { @operation({ summary: "Create", }) @post("{id}") static create = procedure().handle(() => null); } controllers["One0Controller"] = One0Controller; initSegment({ segmentName: "", emitSchema: true, controllers, }); }); bench.add("No-op decorators for 1 classes", async () => { const controllers: Record<string, Function> = {}; @noopClassDecorator() class One0Controller { @noopDecorator({ summary: "Create", }) @noopDecorator("{id}") static create = () => { return null; }; } });

Methodology (short)

For N ∈ {1, 10, 100, 1,000, 10,000} measure:

  • App creation, decorator processing, metadata build and initSegment().
  • Compare with equivalent classes using no-op decorators, to isolate the framework’s work.

Example no‑op decorators:

function noopDecorator() { return function (..._args: any[]) {}; } function noopClassDecorator() { return function <T extends new (...a: any[]) => any>(c: T) { return c; }; }

Results

ControllersVovk.ts Init Time (med)No-op Time (med)Overhead RatioThroughput (ops/s)
16.896 μs0.583 μs11.8x145,023
1064.750 μs5.229 μs12.4x15,444
100664.021 μs56.251 μs11.8x1,507
1,0008,000.167 μs698.417 μs11.5x125
10,000113,550.011 μs18,652.260 μs6.1x9

Key takeaways:

  • Linear growth: about 6.5–8 µs per controller up to 1,000 controllers, about 11 µs per controller at 10,000.
  • At 10,000 classes, garbage collection dominates the no-op run, so its ratio is low.
  • Absolute times are small for long-running services, and acceptable for serverless at typical sizes.

Practical guidance

  • For high-performance workloads, split the app into several segments (serverless functions built from Next.js route.ts files), each with up to 1,000 procedures.
  • In theory, with well-planned segments and enough hardware, one Next.js/Vovk.ts app can host up to ~100,000 procedures. Test this in your environment: the real limits are memory, bundle size, cold start budgets and platform quotas.

Benchmarks: Tinybench on Node.js 24.1, on an Apple M4 Pro, 2026-10-06. Numbers can vary with the runtime, hardware and build settings. Scripts: https://github.com/finom/vovk/tree/main/perf 

Last updated on