dd-trace
dd-trace is Datadog's official APM tracer for Node.js. You load it before the rest of your application and it patches supported libraries at require time (http, express, pg, redis, kafkajs, and dozens more), so every request produces a distributed trace without per-library code. Spans are sent to a Datadog Agent process running next to your app on port 8126, and the Agent forwards them to Datadog's backend. The same package also carries most of Datadog's other Node.js products behind config flags: runtime metrics, the continuous profiler, log-trace correlation, AppSec and IAST, dynamic instrumentation, and LLM observability. It is not a general-purpose tracing library: spans go only to the Datadog Agent, and the Agent needs an API key from a Datadog account, so without a subscription the data has nowhere to go.
If your company pays for Datadog, this is the correct, actively maintained way to get Node.js traces into it, and the preload and ESM flags are the only real friction. If you are not a Datadog customer it does nothing for you, and even if you are, OpenTelemetry with Datadog's OTLP intake is the more portable long-term bet.
Use it if
- Your company already pays for Datadog and you want Node.js services in the same APM view as your other services, with the least instrumentation code
- You want auto-instrumentation of a long list of frameworks and clients from a single preload flag instead of wiring each library by hand
- You need the extras in one dependency: runtime metrics, continuous profiler, log injection, and AppSec all ship inside this package behind flags
- You run polyglot services and need distributed traces to cross language boundaries, which Datadog's tracers handle by propagating context in headers automatically
- You do not have a paid Datadog account: the tracer exports only to the Datadog Agent, there is no Jaeger, Zipkin, or OTLP exporter, so without a subscription this package is dead weight
- You want vendor-neutral instrumentation you can point elsewhere later: @opentelemetry/sdk-node keeps you portable, and Datadog ingests OTLP anyway, so choosing dd-trace is a lock-in decision, not a necessity
- Install weight matters to you: the package unpacks to about 7 MB across 1129 files and pulls in ten optional dependencies, several of them native modules (@datadog/pprof, native-appsec, native-metrics), which cold-starting Lambdas and slim containers will feel
- You bundle your server with webpack or esbuild and will not follow their bundler docs: bundlers inline requires, which silently defeats the require-patching, so you need their esbuild or webpack plugin and the native modules kept external
- You are stuck below Node 22 and want the current line: v6 requires Node >= 22, and the v5 line (Node >= 18) is already in maintenance with end of life scheduled for July 2027
Setup reality
npm install is the easy part; loading order is the real setup. The tracer must load before every module it instruments, and the reliable way is NODE_OPTIONS='--require dd-trace/init', because in transpiled TypeScript your imports hoist above a tracer.init() call in the same file and you silently lose instrumentation. ESM apps need a different flag entirely, node --import dd-trace/initialize.mjs, and picking the wrong ESM entry point (register.js registers hooks but never calls init) is a common way to get zero traces with zero errors. Nothing appears until a Datadog Agent is reachable on port 8126, so local dev and CI need the agent container, and runtime metrics additionally need DogStatsD on port 8125. Release cadence is roughly weekly across two maintained lines (6.9.0 and 5.120.0 both shipped on 2026-08-05), so expect frequent dependabot noise.
Patterns
Load the tracer before everything with --requireinit-preload-cjs
# CommonJS apps: preload so nothing can beat it
NODE_OPTIONS='--require dd-trace/init' node server.js
# equivalent direct form
node --require dd-trace/init server.jsdd-trace patches modules at require time, so it must load before express, pg, and friends. dd-trace/init calls init() for you, configured purely from DD_* env vars. If a library loads first you simply get no spans for it, with no error telling you why.
Initialize in code without the import-order trapinit-programmatic
// tracer.js
const tracer = require('dd-trace').init({
service: 'checkout-api',
env: 'production',
version: '1.4.2'
})
module.exports = tracer
// server.js, very first line:
require('./tracer')
const express = require('express')In TypeScript and ESM, import statements hoist above any code in the same file, so calling init() at the top of your entry file still runs after your other imports have loaded. Keep init in its own module imported first, or skip the whole problem with --require.
Instrument an ESM app with --importesm-import
node --import dd-trace/initialize.mjs server.mjs
# or via env, useful in Docker CMD
NODE_OPTIONS='--import dd-trace/initialize.mjs' node server.mjsESM needs loader hooks, and initialize.mjs both registers them and calls init(). dd-trace/register.js registers the hooks but never initializes the tracer, and the legacy --loader dd-trace/loader-hook.mjs form also exists, so the wrong entry point gives you a silent no-op.
Set service, env, and version the Datadog wayunified-service-tagging
DD_SERVICE=checkout-api \
DD_ENV=production \
DD_VERSION=1.4.2 \
NODE_OPTIONS='--require dd-trace/init' node server.jsThese three tags are how Datadog joins traces, logs, metrics, and deploy tracking for one service. Prefer env vars so the same build can be promoted between environments; if you set both, programmatic init options win over the env vars.
Create a custom span around a functioncustom-span-trace
const tracer = require('dd-trace')
async function chargeCard (order) {
return tracer.trace('charge.card', { resource: order.gateway }, async (span) => {
span.setTag('order.id', order.id)
return gateway.charge(order)
})
}trace() activates the span for everything inside the function, finishes it when the returned promise settles, and tags the error on rejection. This is the API you want most of the time; startSpan is for when start and finish happen in different places.
startSpan when begin and end live in different placesmanual-span-activate
const span = tracer.startSpan('queue.process', {
tags: { 'queue.name': 'emails' }
})
try {
await tracer.scope().activate(span, () => handle(job))
} catch (err) {
span.setTag('error', err)
throw err
} finally {
span.finish()
}startSpan does not activate the span: auto-instrumented calls and child spans only nest under it inside scope().activate(). Nothing finishes it for you either, and an unfinished span holds its whole trace back from being reported.
Tag the active span from anywhereadd-span-tags
const span = tracer.scope().active()
if (span) {
span.setTag('customer.plan', user.plan)
span.setTag('cart.size', cart.items.length)
}
// tags on every span, no code:
// DD_TAGS=team:payments,region:eu-west-1active() returns null outside any span (startup code, detached timers), so always guard it. Use DD_TAGS for tags that every span from the process should carry instead of repeating setTag calls.
Turn off noisy or unwanted integrationsdisable-integrations
const tracer = require('dd-trace').init()
tracer.use('dns', false)
tracer.use('net', false)
// or without touching code:
// DD_TRACE_DISABLED_INSTRUMENTATIONS=dns,netdns and net spans mostly duplicate what the http spans already tell you. Be careful disabling integrations that carry context between services, like http or messaging clients: you also lose distributed trace propagation through them.
Correlate logs with traceslog-injection-correlation
const tracer = require('dd-trace').init({
logInjection: true // or DD_LOGS_INJECTION=true
})
const pino = require('pino')
const logger = pino()
logger.info('charging card')
// record now includes dd.trace_id, dd.span_id, dd.service, dd.env, dd.versionOff by default, and it only works with loggers the tracer instruments (pino, winston, bunyan), which therefore must also load after the tracer. The correlation only pays off if the logs are shipped to Datadog too.
Enable runtime metrics and the continuous profilerruntime-metrics-profiling
const tracer = require('dd-trace').init({
runtimeMetrics: true, // DD_RUNTIME_METRICS_ENABLED=true
profiling: true // DD_PROFILING_ENABLED=true
})Runtime metrics (event loop, GC, heap) leave over DogStatsD on port 8125, a separate port from the 8126 trace intake, so the agent must have it open. Profiling relies on the optional @datadog/pprof native module and shows up in Continuous Profiler, which Datadog bills separately from APM.
Control sample rate and drop noisy endpointssampling-config
const tracer = require('dd-trace').init({
sampleRate: 0.2, // DD_TRACE_SAMPLE_RATE
samplingRules: [
{ sampleRate: 1, name: 'charge.card' },
{ sampleRate: 0, resource: 'GET /healthz' }
],
rateLimit: 100 // DD_TRACE_RATE_LIMIT
})Rules match on service, name, resource, and tags; a sampleRate of 0 drops matches, which is the standard way to keep health checks out of your ingested volume. With no sampling config at all, the rate is deferred to the agent.
Bundle with esbuild without losing instrumentationesbuild-bundle
// build.js
const ddPlugin = require('dd-trace/esbuild')
require('esbuild').build({
entryPoints: ['server.js'],
bundle: true,
platform: 'node',
outfile: 'dist/server.js',
plugins: [ddPlugin],
external: ['@datadog/native-metrics', '@datadog/pprof', '@datadog/native-appsec']
})Bundlers inline requires, which defeats the tracer's module patching, so the plugin is mandatory, not an optimization. The @datadog/* native binaries cannot be bundled and must stay external and present in node_modules at runtime. v6 also ships a dd-trace/webpack plugin for webpack builds.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @opentelemetry/sdk-node | npm | You want vendor-neutral auto-instrumentation; Datadog accepts OTLP, so you can keep the backend and drop the lock-in |
| newrelic | npm | Your org is on New Relic instead: same all-in-one vendor agent model, different backend |
| elastic-apm-node | npm | You run the Elastic stack and want traces in Kibana rather than a separate SaaS bill |
| prom-client | npm | You only need metrics and dashboards, not distributed tracing, and want to stay off paid APM entirely |