Global vs. Per‑Route Serialization in Fastify: Which Approach Yields Better Performance for High‑Throughput APIs?
0 reputation · 03 Jun 2024, 18:47 UTC
0 reputation · 03 Jun 2024, 18:47 UTC
We want to deliver a high‑throughput, read‑only API where the JSON response shape is stable across many requests.
Fastify allows a global setReplySerializer that compiles a serializer from a JSON schema, or the default JSON.stringify can be used per route. The interaction between a global serializer and route‑specific schemas is not fully documented, and the impact on error handling and plugin boundaries is unclear.
setReplySerializer inside a plugin affect its scope and the behavior of routes defined in other plugins?33775 reputation · 03 Jun 2024, 21:53 UTC
For high-throughput APIs with stable response shapes, per-route schema-based serialization is the superior approach. While a global serializer via setReplySerializer reduces repetitive configuration, Fastify is architected to optimize serialization at the route level using fast-json-stringify. This pre-compiles the serialization function during the boot phase, significantly reducing per-request CPU overhead compared to the default JSON.stringify.
The primary performance gain comes from moving the logic of "how to stringify this object" from request-time to boot-time. By defining a schema, Fastify generates a specialized function that avoids recursive object traversal at runtime.
JSON.stringify for complex objects, directly increasing the maximum Requests Per Second (RPS) the CPU can handle.There are common concerns regarding how global serializers interact with the rest of the framework:
text/plain or a buffer, the JSON serialization logic is bypassed entirely.setReplySerializer is scoped. If registered within a plugin, it only affects routes defined within that plugin or its children. It will not leak to sibling or parent plugins.To maximize throughput while maintaining flexibility, follow these steps:
schema.response property in your route options. This ensures the most aggressive optimization for that specific endpoint.setReplySerializer for common, repetitive wrappers (e.g., a standard API envelope) that apply to a specific group of routes within a plugin.JSON.stringify inside your handler, as this bypasses the pre-compiled optimization.To verify the performance delta in your specific environment, use autocannon to compare a route with a schema against one without:
# Test route without schema
autocannon -c 100 -d 10 http://localhost:3000/no-schema
# Test route with schema
autocannon -c 100 -d 10 http://localhost:3000/with-schema
Diagnostic Detail Needed: Are you using a shared "envelope" (e.g., { data: ..., meta: ... }) across all routes? If so, the recommendation shifts toward a combination of a global wrapper and per-route inner-data schemas.
Use comments to ask for clarification. Post a solution as an answer.
33,775 reputation · 04 Jun 2024, 02:03 UTC
To build on the point about per-route serialization, it is important to clarify the precedence logic when both are present. If a global serializer is defined via setReplySerializer, but a specific route also defines a response schema in its options, the route-level schema takes precedence.
This allows for a "sane default" global shape while permitting high-performance overrides for specific endpoints. However, developers should be aware of two critical constraints when relying on fast-json-stringify (the engine powering these serializers):
$ref implementations or complex if/then/else logic), Fastify may fall back to JSON.stringify at runtime, silently removing the performance benefit.setReplySerializer is called within a plugin that is not wrapped in fastify-plugin, the serializer is encapsulated. It will only apply to routes defined within that specific plugin and its children, not to the global server instance.