Monitoring Node.js Applications: APM, Logging, and Error Tracking

Production Node.js applications fail in ways that local testing never reveals. Memory leaks accumulate over hours. Third-party API latency spikes cascade through your request chain. Uncaught exceptions terminate the process without a trace. A proper monitoring stack—combining APM, structured logging, and error tracking—provides the observability needed to detect, diagnose, and resolve issues before they impact users.
APM: Application Performance Monitoring
Application Performance Monitoring tools like Datadog, New Relic, and OpenTelemetry provide end-to-end visibility into request traces, database query performance, and external API calls. OpenTelemetry is the open standard that many modern monitoring platforms build on:
// instrumentation.ts (Next.js or generic Node.js)
import { NodeSDK } from '@opentelemetry/sdk-node'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { Resource } from '@opentelemetry/resources'
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'my-app',
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV,
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
}),
instrumentations: [getNodeAutoInstrumentations()],
})
sdk.start()
With auto-instrumentation, every incoming HTTP request, outgoing fetch call, database query, and Redis operation is automatically traced without modifying your application code. Traces reveal the exact millisecond breakdown of each request, showing whether the bottleneck is your database, an external API, or your own logic.
Structured Logging with Pino
Console logs are insufficient for production debugging. Structured logging outputs each log entry as a JSON object with consistent fields for indexing, filtering, and searching in log aggregation tools:
import pino from 'pino'
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport:
process.env.NODE_ENV === 'development'
? { target: 'pino-pretty' }
: undefined,
redact: {
paths: ['req.headers.authorization', 'req.body.password', 'user.email'],
censor: '[REDACTED]',
},
})
// Usage
logger.info({ userId, action: 'checkout.completed', orderTotal: 49.99 }, 'Order processed')
logger.error({ err, requestId }, 'Payment processing failed')
Key practices for structured logging:
- Include
requestIdortraceIdin every log entry to correlate logs across services - Never log raw SQL queries or user passwords—use Pino's
redactconfiguration - Use consistent field names (
userId,err,durationMs) across your codebase - Set appropriate log levels:
errorfor failures,warnfor degraded states,infofor business events
Error Tracking with Sentry
Sentry captures unhandled exceptions and rejected promises, enriches them with source maps, and groups similar errors automatically. Integration with Next.js is straightforward:
// sentry.config.ts
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.25, // sample 25% of transactions
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [Sentry.httpIntegration(), Sentry.prismaIntegration()],
})
Sentry's performance tracing shows the exact line of code that threw an exception, along with the request context, browser information, and the state of the application at the time of the error. Use Sentry.captureException for caught errors you want to track without crashing:
try {
await processPayment(paymentIntent)
} catch (error) {
Sentry.captureException(error, {
extra: { paymentIntentId: paymentIntent.id, userId },
})
return NextResponse.json({ error: 'Payment failed. Please try again.' }, { status: 500 })
}
Performance Profiling and Memory Leak Detection
Node.js memory leaks are subtle and often take hours to manifest. Use heap snapshots to diagnose growing memory usage:
import v8 from 'v8'
import fs from 'fs'
export function takeHeapSnapshot() {
const snapshotStream = v8.getHeapSnapshot()
const fileStream = fs.createWriteStream(`/tmp/heap-${Date.now()}.heapsnapshot`)
snapshotStream.pipe(fileStream)
}
Trigger heap snapshots automatically when RSS memory exceeds a threshold:
const MEMORY_THRESHOLD_MB = 512
setInterval(() => {
const usage = process.memoryUsage()
if (usage.rss > MEMORY_THRESHOLD_MB * 1024 * 1024) {
logger.warn({ rss: usage.rss }, 'Memory threshold exceeded')
takeHeapSnapshot()
}
}, 60_000)
Load the .heapsnapshot file into Chrome DevTools to inspect retained objects, detached DOM nodes, and closure variables that prevent garbage collection.
Alerting Strategies
Monitoring without alerting is just logging. Define tiered alert rules:
- Critical (SMS/call): 5xx error rate above 5% for 5 minutes, process down, p99 latency > 10s
- Warning (email): Memory > 80%, error rate > 1%, slow queries > 1s
- Info (dashboard): Deployments, traffic spikes, cache hit ratio changes
Use OpenTelemetry metrics to power your alerting:
import { metrics } from '@opentelemetry/api'
const meter = metrics.getMeter('my-app')
const requestDuration = meter.createHistogram('http.request.duration_ms', {
description: 'HTTP request duration in milliseconds',
unit: 'ms',
})
// In middleware
requestDuration.record(duration, {
method: req.method,
route: req.url,
status: res.statusCode,
})
A well-instrumented Node.js application gives you visibility into every layer of the stack. APM traces reveal request bottlenecks, structured logs provide debug context, error tracking captures production failures, and profiling detects memory issues before they cause outages.
<a href="/services/web-development">Our team builds and monitors production Node.js applications</a> with observability best practices baked in. Contact us to discuss your monitoring strategy.
Related Insights

Building CLI Tools with Node.js: From Zero to Published npm Package
A complete guide to building and publishing CLI tools with Node.js including argument parsing, interactive prompts, color output, error handling, and npm distribution.

Monitoring and Observability with OpenTelemetry: Traces, Metrics, and Logs
Learn how to implement observability with OpenTelemetry including distributed tracing, metric collection, log correlation, and building dashboards with Grafana.

Web Performance Budgets: Setting and Enforcing Performance Targets
Learn how to set and enforce web performance budgets including bundle size limits, image weight budgets, third-party script caps, and CI enforcement.