How I Added APM to a Node.js App Using New Relic

Before New Relic, debugging a slow API in production meant one thing, staring at console.log output and guessing.
Someone reports “the order page is slow.” You have no idea if it’s a slow query, a slow external call, or just one endpoint under load. So you add some logs, deploy, wait for it to happen again, and hope the logs tell you something.
New Relic fixed that. This is how we set it up and what actually matters when you do.
What New Relic Does
It sits inside your Node.js process and automatically records:
- Every HTTP request - which route, how long, whether it errored
- Every database query - the SQL, how long it took, which API triggered it
- Every Kafka and SQS message processed - topic, duration, errors
- Every crash - full stack trace, which request caused it
All of it shows up on a dashboard at newrelic.com in almost real time. No console.log.
The reason it works with almost zero code for most things is auto-instrumentation. When you require('newrelic') at startup, it patches express, pg, and redis at the module level. From that point, every DB query and every HTTP request is tracked automatically.
Why New Relic - and Is It Right for You?
A few honest reasons we picked it:
- Free tier is generous. 100GB data ingest per month, one free user, no credit card to start. A small to mid-sized Node.js service will comfortably stay under that.
- Almost zero setup for what matters most. DB query tracing and HTTP route timing work the moment you add one require. No per-route or per-query instrumentation needed.
- Supports our exact stack out of the box. express, pg, redis are auto-instrumented. Kafka, SQS, and Socket.io need a small manual wrapper - covered below, but the agent understands them natively.
That said, if you’re already deep in AWS, X-Ray works. If you want full control and don’t mind running your own backend, OpenTelemetry + Jaeger is free. If budget isn’t a concern, Datadog has more features. New Relic made sense for us because it worked quickly, covered our whole stack, and cost nothing at our scale.
The One Thing You Must Get Right
require('newrelic') must be the first thing that runs. Before Express. Before pg. Before anything.
// index.js — this order is non-negotiable
require('dotenv').config();
if (process.env.SERVER_NAME === 'PROD') {
require('newrelic');
}
const express = require('express');
// rest of your app...
If express or pg loads before newrelic, the agent can't patch them. Auto-instrumentation silently fails, and you'll wonder why nothing shows up in the dashboard.
We gate it behind SERVER_NAME === 'PROD' so dev and QA traffic doesn't pollute the dashboard, and we don't burn the free tier on test data.
Configuration
Create newrelic.js in your project root:
exports.config = {
app_name: ['Your App Name'],
license_key: process.env.NEW_RELIC_LICENSE_KEY,
logging: {
level: 'info',
filepath: 'stdout'
},
distributed_tracing: {
enabled: true
},
transaction_tracer: {
enabled: true,
trace_threshold: 'apdex_f',
record_sql: 'obfuscated'
},
error_collector: {
enabled: true,
record_db_errors: true
}
}A few settings worth understanding:
record_sql: 'obfuscated'-New Relic captures your SQL but replaces actual values with ?. So instead of sending WHERE user_id = 12345 AND token = 'abc123' to New Relic's servers, it sends WHERE user_id = ? AND token = ?. You still see which queries are slow, and their structure, but no sensitive data leaves your server.
trace_threshold: 'apdex_f'- Only transactions considered "frustratingly slow" by the Apdex standard get a full detailed trace (which query ran, from which line). Faster transactions are still counted in metrics, but without the detailed breakdown.
logging.filepath: 'stdout'-Agent diagnostic logs go to console output instead of being written to newrelic_agent.log on disk. Without this, that file grows indefinitely on a long-running server.
error_collector.record_db_errors: true-Database-level errors (query timeouts, constraint violations) get forwarded to New Relic's error inbox automatically, without needing a manual noticeError() call.
The Safety Wrapper
Express routes are auto-instrumented. But Kafka, SQS, and Socket.io aren’t - those need manual wrapping. And those wrappers need to do nothing gracefully when New Relic isn’t loaded (dev, QA, or if the package fails to load).
Create helpers/nrWrap.js:
const newrelic = (() => {
if (process.env.SERVER_NAME !== 'PROD') return null;
try {
return require('newrelic');
} catch (err) {
return null;
}
})();const addCustomAttribute = (key, value) => {
try {
if (newrelic) newrelic.addCustomAttribute(key, value);
} catch (e) {}
};
const noticeError = (err) => {
try {
if (newrelic) newrelic.noticeError(err);
} catch (e) {}
};
module.exports = { newrelic, addCustomAttribute, noticeError };The SERVER_NAME check matters here. Without it, require('newrelic') actually succeeds in dev - the package is installed, so it loads fine. It just runs without a license key and silently initialises. The SERVER_NAME guard is what actually stops it from running.
Every file that needs New Relic imports from here:
const { newrelic, addCustomAttribute, noticeError } = require('../helpers/nrWrap');Tracking Kafka Messages
HTTP routes work automatically. Kafka messages don’t - they arrive outside any HTTP context, so New Relic doesn’t know they exist unless you tell it.
eachMessage: async ({ topic, partition, message }) => {
const run = async () => {
try {
addCustomAttribute('kafkaTopic', topic);
addCustomAttribute('kafkaPartition', String(partition));
await yourMessageHandler({ topic, partition, message });
} catch (err) {
noticeError(err);
throw err;
}
};
if (newrelic) {
await newrelic.startBackgroundTransaction(`kafka/${topic}`, 'Kafka', async () => {
await run();
});
} else {
await run();
}
}startBackgroundTransaction creates a transaction context for the message. Any DB queries that run inside run() automatically appear as segments under that transaction - same as an HTTP request.
Tracking SQS Messages
Same pattern:
if (newrelic) {
await newrelic.startBackgroundTransaction(`sqs/${queueName}`, 'SQS', async () => {
await run();
});
} else {
await run();
}Tracking Socket.io Events
if (newrelic) {
await newrelic.startBackgroundTransaction(`socket/${key}`, 'Socket', async () => {
await run();
});
} else {
await run();
}
});
}Reporting Caught Errors
By default, New Relic only sees unhandled errors. If you catch and handle an error, New Relic never knows it happened. Use noticeError to send it anyway:
try {
await riskyOperation();
} catch (err) {
noticeError(err); // sends to New Relic with full context
// handle normally
}Useful for things like failed Kafka publishes or third-party API errors that you handle gracefully but still want visibility into.
What You Can Actually See
Which API is slowest - APM → Transactions → sort by average response time.
Why a specific API is slow - Click any transaction → Transaction traces. A waterfall of every DB query that ran, how long each took, and at what point in the request.
Which SQL query is slowest across all APIs - APM → Databases. Every query pattern ranked by average execution time, with which routes triggered it.
Errors APM → Errors. Every error with a full stack trace, which route or background job caused it, and when.
That’s the full setup. One require, one config file, one wrapper module, and a small manual wrapper for anything outside HTTP. After that, the slow APIs and slow queries surface themselves - you don't have to hunt for them anymore.