Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome to your penultimate lesson.
In our last session, we implemented a robust structured logging system using Pino. This gives us detailed, event-by-event insight into our server's operations. Now, we'll build on that by looking at the bigger picture: performance over time. While logs tell us what happened, metrics tell us how well it's happening, in a quantitative and aggregated way.
Today, we will address the learning outcome: Monitor server-side performance metrics, including request and response times. We'll explore how to capture, store, and visualize key performance indicators (KPIs) like request rates, error rates, and response durations. This is essential for understanding application health, identifying bottlenecks, and ensuring a good user experience.
We will cover three approaches, progressing from a simple built-in method to a full-fledged, industry-standard monitoring stack.
1. The Built-in Approach: Simple Metrics Collection
TanStack Start's architecture, particularly its middleware system, provides a straightforward way to collect basic performance data without external dependencies. This approach is great for getting started and for environments where a simpler setup is preferred.
The idea is to expand our existing middleware to not only log request durations but also to record them in a simple in-memory collector. We can then expose this data via a dedicated /metrics endpoint.
Observability | TanStack Start React Docs
The official TanStack Start documentation provides excellent patterns for this kind of built-in observability. Let's review the relevant sections.
Please read the following sections: Request/Response Middleware: You've seen this pattern in our logging lesson. Notice again how it captures the request duration. Performance Metrics Collection: Pay close attention to the MetricsCollector class. It's a simple but effective way to store timings and calculate basic statistics like average, p50 (median), and p95.
Now, let's integrate these concepts.
1.1. Creating a Metrics Collector
First, create a utility file for the MetricsCollector class.
// src/lib/metrics.ts
export class MetricsCollector {
private metrics = new Map<string, number[]>();
recordTiming(name: string, duration: number) {
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
this.metrics.get(name)!.push(duration);
}
getStats(name: string) {
const timings = this.metrics.get(name) || [];
if (timings.length === 0) return null;
const sorted = [...timings].sort((a, b) => a - b);
const sum = timings.reduce((a, b) => a + b, 0);
return {
count: timings.length,
avg: sum / timings.length,
p50: sorted[Math.floor(sorted.length * 0.5)],
p95: sorted[Math.floor(sorted.length * 0.95)],
min: sorted[0],
max: sorted[sorted.length - 1],
};
}
getAllStats() {
const stats: Record<string, ReturnType<typeof this.getStats>> = {};
for (const [name] of this.metrics.entries()) {
stats[name] = this.getStats(name);
}
return stats;
}
}
export const metrics = new MetricsCollector();
1.2. Updating the Middleware
Next, we'll update the loggingMiddleware from our previous lesson to use this collector.
// src/server/loggingMiddleware.ts
import { createMiddleware } from '@tanstack/start';
import { randomUUID } from 'node:crypto';
import logger from '~/lib/logger';
import { metrics } from '~/lib/metrics'; // Import our collector
// ... (RouteContext type declaration)
export const loggingMiddleware = createMiddleware().handler(
async ({ request, next }) => {
const start = Date.now();
const requestId = request.headers.get('x-request-id') || randomUUID();
const reqLogger = logger.child({ requestId });
reqLogger.info({ /* ... */ }, 'Request started');
const response = await next({ logger: reqLogger });
const duration = Date.now() - start;
// Record the timing
metrics.recordTiming('http_request_duration_ms', duration);
reqLogger.info({ status: response.status, duration }, 'Request finished');
response.headers.set('x-request-id', requestId);
return response;
},
);
1.3. Exposing the Metrics Endpoint
Finally, create a route to expose the collected data, as shown in the documentation.
// src/routes/metrics.tsx
import { createFileRoute, json } from '@tanstack/react-router';
import { metrics } from '~/lib/metrics';
import { lazy } from 'react';
export const Route = createFileRoute('/metrics')({
// This is a server-only route
server: {
handlers: {
GET: async () => {
return json({
system: {
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: new Date().toISOString(),
},
application: metrics.getAllStats(),
});
},
},
},
// We need a component, but it will never be rendered on the client for a GET
component: lazy(() => import('~/components/EmptyComponent')),
});
Now, if you run your app and make some requests, navigating to /metrics will show you the aggregated statistics. This is a solid first step, but its in-memory nature means metrics are lost on every server restart. For production, we need something more robust.
2. The Production Standard: Prometheus and Grafana
For a production-grade setup, we'll use two industry-standard tools:
- Prometheus: An open-source monitoring system and time-series database. It works by "scraping" (pulling) metrics from an HTTP endpoint on your application.
- Grafana: An open-source platform for data visualization, which allows you to create powerful dashboards from data sources like Prometheus.
Since your goal includes using Bun, we'll follow a guide tailored for a Bun backend.
2.1. Instrumenting the App for Prometheus
First, we need to modify our application to expose metrics in a format Prometheus understands. We'll use the prom-client library.
Setting up a Prometheus and Grafana Monitoring System for my Bun.js Backend
This article provides a fantastic, practical walkthrough of setting up Prometheus and Grafana for a Bun.js application. We'll start by instrumenting our app.
Follow 'Step 1: Configure Your Backend to Expose Metrics'. Install prom-client and create the metrics middleware. Notice the two key metrics being created: Counter: For http_requests_total, which only ever increases. Histogram: For http_request_duration_seconds, which groups observations into configurable buckets. This is what allows for calculating accurate latency percentiles (p95, p99).
You can adapt the middleware from the guide and integrate it into your TanStack Start application, similar to how we added the logging middleware. The key is to apply it to all routes so it can track every request.
2.2. Running the Monitoring Stack
With the app exposing a /metrics endpoint, we need to set up Prometheus to scrape it and Grafana to visualize it. The easiest way to run these services is with Docker.
Setting up a Prometheus and Grafana Monitoring System for my Bun.js Backend
Now, let's get the monitoring infrastructure running.
Please follow these steps from the guide: Step 2: Configure Prometheus: Create the prometheus.yml file. The scrape_configs section is the most important part; it tells Prometheus where your application's /metrics endpoint is. Step 3 & 5: Start Prometheus and Grafana with Docker: Use the provided docker run commands to start the containers. Step 6 & 7: Configure Grafana: Log in to Grafana and add Prometheus as a data source. This connects your visualization layer to your metrics database.
At this point, you have a complete pipeline: your TanStack Start app generates metrics, Prometheus collects and stores them, and Grafana is ready to query and display them.
2.3. Building Dashboards and Querying Metrics
The final step is to visualize the data. Grafana uses Prometheus's query language (PromQL) to create graphs.
Setting up a Prometheus and Grafana Monitoring System for my Bun.js Backend
Let's look at how to query our data and what the results mean.
Read 'Step 9' and the 'Final Result' section. Pay attention to the example PromQL queries for key metrics like: Request Rate: rate(http_requests_total[1m]) Error Rate: A similar rate query, but filtered by status code. 95th Percentile Response Time: histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) Also, review the interpretation of the CPU and Memory graphs to understand how to derive insights from the visualizations.
This setup provides a powerful, scalable, and persistent monitoring solution that is the standard for many production systems.
3. The Future: Unified Observability with OpenTelemetry
Prometheus is excellent for metrics, but modern observability often involves correlating metrics with logs and traces (detailed, step-by-step records of a request's journey). OpenTelemetry (OTel) is an emerging CNCF standard designed to unify all three "pillars of observability".
While a full OTel setup is beyond this lesson's scope, it's crucial to understand the concepts, as the industry is rapidly moving in this direction. In fact, the TanStack team has noted that direct OpenTelemetry support is a future goal for TanStack Start.
OpenTelemetry in Node.js - Traces, Metrics and Logs
This video from Better Stack provides a concise and practical demonstration of setting up OpenTelemetry in a Node.js application. It clearly shows the power of combining traces, metrics, and logs.
Please watch the following segments to grasp the core concepts of OpenTelemetry: Setting up and Auto-Instrumentation (01:32 - 10:26): See how OTel can automatically instrument your HTTP server to generate traces and metrics with minimal code. Implementing Custom Metrics (20:50 - 26:49): Notice how you can create custom metrics like counters and histograms, similar to prom-client. Exporting Data (30:13 - 37:29): Understand the role of an OTel Collector and how data is sent to a backend for visualization. This decouples your application from the specific monitoring tool you use.
Understanding OpenTelemetry positions you at the forefront of modern application monitoring, preparing you for the next generation of observability tools that will likely be integrated directly into frameworks like TanStack Start.
Conclusion
Today you've learned how to monitor the performance of your server-side application, a critical skill for maintaining a healthy and reliable service.
Key Takeaways:
- Metrics vs. Logs: Logs record discrete events, while metrics provide quantitative, aggregated data about performance over time.
- Built-in vs. Dedicated Tools: You can start with simple, in-memory metrics collection, but production systems benefit from robust tools like Prometheus for storage and Grafana for visualization.
- Key Metrics: Request rate, error rate, and latency percentiles (p50, p95, p99) are fundamental indicators of application health.
- The Future is Unified: OpenTelemetry is the emerging standard for combining metrics, logs, and traces into a single, correlated view of your application's behavior.
This lesson concludes our module on Production Deployment and Monitoring. You are now equipped not only to build and deploy a TanStack Start application but also to operate it responsibly with proper error tracking, logging, and performance monitoring.
This also marks the end of our course. Congratulations! You have journeyed from the foundational trade-offs of SSR, through the intricacies of Tanstack Router and Start, to advanced data management, migration strategies, and finally, production-grade deployment and observability. You have built a comprehensive skill set to tackle the primary goal you set out with: transforming a SPA into a modern, performant SSR application.
Well done on your dedication and progress throughout these lessons. The next step is to apply this knowledge to your own projects. Good luck