
In part one of this series, we set up basic performance monitoring for our Fastify application using AppSignal and explored key performance indicators.
Now that we have our monitoring foundation in place, it's time to leverage these insights to actively improve application performance.
You'll learn how to detect performance regressions, find optimization opportunities, and implement custom instrumentation with OpenTelemetry.
I'll show you how to use dashboards in AppSignal to identify bottlenecks, compare performance across deployments, and set up application monitoring beyond performance tracking.
Let's get started!
Detecting Performance Regressions in Fastify for Node
It's not enough to track performance metrics alone; you also need to understand how your application's performance evolves over time. When new code introduces regressions, you want to identify and fix them quickly.
The first step toward detecting regressions is linking performance data to
specific application versions. If you're using Git, connect the specific commit
hash to your performance data by modifying your appsignal.js file:
// appsignal.js import childProcess from "node:child_process"; import { Appsignal } from "@appsignal/nodejs"; const REVISION = childProcess.execSync("git rev-parse --short HEAD").toString(); new Appsignal({ active: true, name: "<Your App Name>", revision: REVISION, // sets the Git revision });
Once enabled, AppSignal automatically creates Deploy Markers for each deployment, allowing you to filter issue data by specific versions:

You can also view performance trends in Summary > Performance in the last deploys, which displays mean response times across deployments along with sample counts:

In this example, the /user-balances route shows significantly worse
performance in the latest deployment, suggesting a potential regression.
From here, you can determine whether this regression affects your entire application or just this specific route by examining recent samples to see the request breakdown, or by comparing source control revisions to identify exactly what changed between versions.
Finding Opportunities for Node.js App Performance Improvements
Thanks to AppSignal's automatic instrumentation with popular Node.js libraries, you can easily identify the most urgent areas for performance improvement in your application.
Under the Performance menu, you'll find Slow API requests and Slow queries. These sections rank your API endpoints and database queries by performance impact, helping you prioritize optimizations for maximum benefit.

The Slow events view aggregates all instrumented events into a single dashboard, allowing you to pinpoint expensive operations across your entire application.

Clicking on an event group like fetch shows you the slowest events in that group:

To see more details, click the Name of the event you're investigating. You'll see performance graphs and, most importantly, the actions where these events occur.

Scroll down to the bottom of this page and click on the most recent incident:

Then find the most recent sample, and click on it:

Analyzing the Event Timeline
In your Timeline, patterns of many small, sequential events often
indicate inefficient
N+1 queries,
as seen in the /customers-with-orders route, where the query count grows with the
number of customers and orders:
fastify.get('/customers-with-orders', async (request, reply) => { try { // [...] // N+1 problem: For each customer, we make a separate query to get their orders for (const customer of customers) { // This is the "+N" part - one additional query per customer const orders = await db('orders') .where('customer_id', customer.customer_id) .select('order_id', 'order_date', 'shipped_date', 'freight'); // Attach orders to each customer customer.orders = orders; // For each order, we make another query to get order details - making this even worse! for (const order of orders) { // [...] } } // [...] } catch (error) { request.log.error(error); return reply.code(500).send({ error: 'Failed to retrieve customer data' }); } });

You can optimize this route to use only three queries regardless of data size:
// server.js fastify.get('/customers-with-orders', async (request, reply) => { try { // 1️⃣ Fetch all customers (basic information only) const customers = await db('customers').select( 'customer_id', 'company_name', 'contact_name' ); // 2️⃣ Fetch all orders for all customers in a single query // Uses WHERE IN to get only the orders belonging to the retrieved customers const orders = await db('orders') .whereIn( 'customer_id', customers.map((c) => c.customer_id) ) .select( 'order_id', 'customer_id', 'order_date', 'shipped_date', 'freight' ); // 3️⃣ Fetch all order details (line items) for all orders in a single query // Joins with the products table to include product names const orderIds = orders.map((o) => o.order_id); const orderDetails = await db('order_details') .whereIn('order_id', orderIds) .join('products', 'order_details.product_id', 'products.product_id') .select( 'order_details.order_id', 'order_details.unit_price', 'order_details.quantity', 'order_details.discount', 'products.product_name' ); // 4️⃣ Organize order details by order_id for faster lookup const orderDetailsMap = {}; orderDetails.forEach((detail) => { if (!orderDetailsMap[detail.order_id]) { orderDetailsMap[detail.order_id] = []; } orderDetailsMap[detail.order_id].push(detail); }); // 5️⃣ Group orders by customer_id and attach order details to each order const ordersByCustomer = {}; orders.forEach((order) => { if (!ordersByCustomer[order.customer_id]) { ordersByCustomer[order.customer_id] = []; } // Attach items (order details) to each order order.items = orderDetailsMap[order.order_id] || []; ordersByCustomer[order.customer_id].push(order); }); // 6️⃣ Attach orders to each customer customers.forEach((customer) => { customer.orders = ordersByCustomer[customer.customer_id] || []; }); // 7️⃣ Return combined customer–order data with total customer count return { customerCount: customers.length, customers, }; } catch (error) { request.log.error(error); return reply.code(500).send({ error: 'Failed to retrieve customer data' }); } });
This route efficiently retrieves all customers and their related orders and order details from a database while avoiding the N+1 query problem by batching lookups. It performs only three SQL queries in total: one to get all customers, one to get all their orders, and one to get all order details.
After making this change, commit it and send more traffic to the endpoint. In a few minutes, you'll see the route's performance improve significantly compared to the previous commit:

Examining a sample reveals the efficiency improvements, with the results now only requiring three SQL queries:

This optimization significantly reduces a route's performance impact, as reflected in the Issues list when viewing the Last deploy:

You can apply similar analysis to other routes like /user-balances,
using techniques such as parallel requests with Promise.all() and
response caching to further improve performance.
Setting up Custom Instrumentation with OpenTelemetry
While automatic instrumentation covers most high-level metrics needed for baseline performance monitoring, you'll often need deeper visibility into specific performance-critical code sections.
AppSignal leverages OpenTelemetry's trace API under the hood, making it straightforward to implement custom performance tracking.
npm install @opentelemetry/api
Get a tracer instance as follows:
import { trace } from "@opentelemetry/api"; const tracer = trace.getTracer("example-app");
With your tracer configured, wrap any performance-critical code section in an
active span:
fastify.get("/example", async (req, res) => { tracer.startActiveSpan("perform some operation", async (span) => { await someOperation(); // track this operation span.end(); // Don't forget to end the span here }); });
Once implemented, these custom spans will appear in your performance timeline, enabling you to:
- Isolate bottlenecks in critical code paths.
- Track performance trends over time.
- Set targeted alerts for performance regressions.
Comprehensive Monitoring with AppSignal: Host Metrics, Logs, and Notifications
Beyond performance tracking, AppSignal offers a suite of integrated monitoring features that provide a complete view of your application's health.
For example, system-level metrics are automatically tracked under Host monitoring > Host metrics, allowing you to determine if slowdowns stem from application code or resource constraints.

You can also enhance your troubleshooting workflow by consolidating your Fastify logs in AppSignal through the Pino transport:
const fastify = Fastify({ logger: { transport: { targets: [ { target: "@appsignal/nodejs/pino", options: { group: "my-app" } }, ], }, }, });
Your logs will then appear in the Logging > All logs section, providing context to performance data and other application behaviors:

Stay ahead of issues with AppSignal's notification capabilities, which allow you to set precise performance boundaries and receive alerts when metrics cross defined thresholds:

Or you can go to Anomaly detection > Triggers to configure triggers and receive alerts when metrics deviate from your configured limits:

Wrapping Up
In this post, we've covered how AppSignal helps you collect relevant metrics from your application, find optimization opportunities, and detect performance regressions quickly.
With these tools and techniques, you're now equipped to maintain the high performance that drew you to Fastify in the first place, even as your application grows in complexity and scale.
Remember that performance monitoring isn't just about collecting data. It's about turning that data into actionable insights that lead to real improvements for your users. With AppSignal, doing this should be a breeze.
Thanks for reading!
Wondering what you can do next?
Finished this article? Here are a few more things you can do:
- Subscribe to our JavaScript Sorcery newsletter and never miss an article again.
- Start monitoring your JavaScript app with AppSignal.
- Share this article on social media
Most popular Javascript articles

Top 5 HTTP Request Libraries for Node.js
Let's check out 5 major HTTP libraries we can use for Node.js and dive into their strengths and weaknesses.
See more
When to Use Bun Instead of Node.js
Bun has gained in popularity due to its great performance capabilities. Let's see when Bun is a better alternative to Node.js.
See more
How to Implement Rate Limiting in Express for Node.js
We'll explore the ins and outs of rate limiting and see why it's needed for your Node.js application.
See more

Damilola Olatunji
Damilola is a freelance technical writer and software developer based in Lagos, Nigeria. He specializes in JavaScript and Node.js, and aims to deliver concise and practical articles for developers. When not writing or coding, he enjoys reading, playing games, and traveling.
All articles by Damilola OlatunjiBecome our next author!
AppSignal monitors your apps
AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

