# hot-shots is a StatsD client that outgrew the Datadog-only version of the protocol

> hot-shots began as a fork of node-statsd and is now a Node.js client for DogStatsD, Telegraf's StatsD server, the OpenTelemetry Collector's StatsD receiver and Etsy's own StatsD server, with Unix domain socket and TCP transports, client-side aggregation, Datadog mode, a mock transport for tests and TypeScript types. The package is version 17.1.1 and supports Node 18 and newer.

**bdeitte/hot-shots** — A Node.js client for Datadog's DogStatsD server, InfluxDB's Telegraf StatsD server, the OpenTelemetry Collector StatsD receiver, and Etsy's StatsD server.

- Repository: https://github.com/bdeitte/hot-shots
- Stars: 566 · Forks: 146
- Language: JavaScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/bdeitte-hot-shots

## One client for four servers that all speak StatsD

hot-shots is a Node.js client for Datadog's DogStatsD server, InfluxDB's Telegraf StatsD server, the OpenTelemetry Collector StatsD receiver and Etsy's StatsD server.

That list is the point of the library. StatsD is a simple line protocol over UDP, and there are several servers that speak it with slightly different dialects. A team that runs a StatsD-compatible agent locally, or wants to move between DogStatsD and the OpenTelemetry Collector without rewriting their instrumentation, needs a client that speaks all of them rather than one that speaks one of them.

The project started as a fork of node-statsd, and the listed differences from that ancestor are the practical feature set: uds and tcp protocol support, TypeScript types, Telegraf support, Datadog mode, events, child clients, debug logging, client-side aggregation, and a long tail of bug fixes.

The changelog is where the full list of changes lives.

The version scheme tells you something about the maturity. The package is at 17.1.1, so this is a long-lived project that has been through many breaking changes, and the documentation has a section on package versioning and security that is worth reading before you pin it.

The runtime requirement is Node 18.x and higher, and when using the TypeScript definitions the library requires TypeScript 4.0 or higher.

The project is JavaScript, MIT licensed, with the last push on 2026-10-12 and no GitHub releases, so the npm version is the number to track.

## Three ways to import it, and one set of options

The example section shows the same library three ways, and the third one is the one to copy if you use TypeScript.

CommonJS:

```javascript
// CommonJS
const StatsD = require('hot-shots');
const client = new StatsD();

client.increment('my_counter');
```

ESM:

```javascript
// ESM
import StatsD from 'hot-shots';
const client = new StatsD();

client.gauge('my_gauge', 123.45);
```

And TypeScript, where the options object is typed:

```typescript
// TypeScript
import StatsD, { type ClientOptions } from 'hot-shots';
const options: ClientOptions = {
  port: 8125,
  globalTags: { env: 'production' }
};
const client = new StatsD(options);

client.histogram('my_histogram', 42)
```

The TypeScript version shows something worth copying, which is that the options type is exported alongside the client, so the options object gets checked against the library's own definition rather than being an untyped literal.

All initialization parameters are optional, and the list is long enough that it is worth knowing which ones matter.

The transport is configured by host, port, protocol, path and stream. The host defaults to being retrieved from the DD_AGENT_HOST environment variable, and the port from DD_DOGSTATSD_PORT with a default of 8125. The protocol option takes tcp, uds or stream, and defaults to udp otherwise. The path is used only when the protocol is uds and defaults to /var/run/datadog/dsd.socket. The stream option is used only when the protocol is stream.

There is an important fallback here. If no transport options are passed, the transport can be configured from environment variables for parity with the official DogStatsD clients, so a service that already sets DD_AGENT_HOST for the official client gets the same behaviour without extra configuration.

The metric name options are prefix and suffix, and both get a period separator added automatically if one is not present, so my_prefix becomes my_prefix. and my_suffix becomes .my_suffix. That detail is the kind of thing that produces silently wrong metric names when you switch libraries, so it is worth reading.

Then there are the tags. tagPrefix defaults to the character # and tagSeparator to a comma, and both carry a note that they do not work with the telegraf option. That is because the Telegraf line protocol uses a different tag format, and the option that switches to it also disables the options that only make sense for the StatsD dialect.

## Datadog mode adds environment-derived tags

The Datadog mode is where the library does something no plain StatsD client does, and it is worth understanding because it changes what your metrics look like on the server side.

The globalTags option takes tags added to every metric, and it can be either an object or a list of tags, with a default of an empty object.

The includeDataDogTags option, defaulting to true, controls whether Datadog tags are added to the global tags. When it is enabled, several tags are appended from the corresponding environment variables if they are set. The dd.internal.entity_id tag comes from DD_ENTITY_ID. The env tag comes from DD_ENV. The service tag comes from DD_SERVICE. And the version tag comes from DD_VERSION.

So if your service already exports DD_SERVICE and DD_ENV, which is the Datadog convention and is what the Kubernetes docs recommend, then every metric gets tagged with them automatically, without you repeating them at each call site.

The DD_TAGS variable is handled separately and more carefully. Tags from DD_TAGS, or its legacy alias DATADOG_TAGS, are added to globalTags. The documented example is that DD_TAGS=rack:1,team:core adds rack:1 and team:core. And if the value contains no comma, whitespace is used as the separator instead, matching dd-trace-js and the Datadog Agent, so DD_TAGS="env:staging service:my-service" adds env:staging and service:my-service. These are applied before the DD_ENV, DD_SERVICE and DD_VERSION mapping.

That ordering detail is not cosmetic. If you set env:staging in DD_TAGS and then DD_ENV is also set, the two would collide, and the resolution order determines which wins. Applying the general tags first means the specific environment variables are authoritative.

The whitespace fallback is also worth noting, because it is a compatibility decision rather than a design choice. Matching dd-trace-js and the Agent means an environment set up for one of those tools works unchanged here, and that is a small kindness that saves a class of support question.

## Errors, congestion and the shutdown path

The error section has three parts, and the third is the one that bites people.

There is an errorHandler option, a function with one argument that is called to handle various errors, with a default of none, in which case errors are thrown or logged to the console.

Then there is the callback semantics, which the documentation has a section of its own, and a congestion error section alongside it.

The congestion error is the interesting one for anyone running this in production. A UDP socket sending to a server that is not draining fast enough will report that its send buffer is full, and that is a condition where the metric is dropped rather than an exception that should crash your process. A client that throws on congestion turns a metrics problem into an outage, which is why the error handler exists as a separate hook rather than as an option to disable the check.

Then there is the section on sending metrics during process shutdown. This is a genuinely hard problem for a UDP client. When your process is exiting, the socket closes, and anything still buffered in the client is lost. The metrics you most want at that moment, the ones that record why the process died, are exactly the ones most likely to be lost.

So the library has a section dedicated to it, and that is worth knowing about before you rely on the client in a service with a graceful shutdown path.

The buffer options relate. The maxBufferSize option, if larger than 0, means metrics are buffered and only sent when the string length is greater than the size, with a default of 0 for udp and tcp and 8192 for uds. The bufferFlushInterval option is the time in milliseconds to always flush any buffered metrics when buffering is in use, with a default of 1000.

The different defaults between transports are a detail worth knowing. A Unix domain socket is a local stream with backpressure and a smaller buffer, so it defaults to buffering, while a UDP socket to a remote host does not.

## Aggregation, the mock transport and the package layout

Three more options shape how the client behaves under load and in tests.

Client-side aggregation means the client can aggregate values before sending them, which is the standard StatsD answer to high-cardinality counters. There is a section on it and a section on flushing buffered metrics.

The mock option is the one that makes testing straightforward. Creating a mock StatsD instance uses a mock transport that does not create real sockets. Stats are not sent to the server but can be read from mockBuffer for testing, with the documented caveat that mockBuffer will keep growing, so you should only use it for testing or clear it out periodically.

That caveat is a small thing that says the mock was designed for tests rather than for a fake production environment. If you were to point a staging environment at it, the buffer would grow without bound.

There are also options that control the shape of the traffic. The sampleRate sends only a sample of the data to StatsD for all StatsD methods, and can be overridden at the method level, with a default of 1. The cacheDns option caches the DNS lookup for the host for the cacheDnsTtl period, and is only used when the protocol is udp, with a default of false and a TTL of 60000 milliseconds.

The globalize option exposes the StatsD instance globally, with a default of false, and the useDefaultRoute option uses the default interface on a Linux system, which is described as useful when running in containers.

The useDefaultRoute option is the kind of detail that only appears when someone has actually deployed it. In a container the default route may point at the wrong interface, and a UDP socket bound to the wrong address sends metrics into a black hole.

The package layout is modern. The exports map has separate conditions for import and require, each with its own types file, pointing at index.mjs and index.js with types.d.mts and types.d.ts. There is also a wildcard export for ./* so that individual files can be imported directly.

The scripts show a project with performance attention. There is a perf script running a perfTest directory, a coverage script using nyc with both lcov and text reporters, and a test script running mocha with the spec reporter and a five second timeout, plus a separate ESM import test. The pretest hook runs the lint, so lint failures block the test run.

There is also an optional dependency on unix-dgram at 2.x, which is the package that provides Unix domain socket support in Node, and it is optional because it is not needed for the UDP path.

## Conclusion

Adopt hot-shots if you are sending metrics from Node and want one client that covers DogStatsD, Telegraf and the OpenTelemetry Collector without writing a transport per backend. Skip it if you only need a single UDP counter to a single StatsD server and none of the extra features, and if you need the full Datadog client feature set, since this is a StatsD client rather than a Datadog SDK. Verify first whether your collector speaks StatsD or something else, that you set the tag format the receiving server expects, since the Telegraf line protocol differs and the tag prefix and separator options do not apply to it, and how you want to handle metrics emitted during process shutdown, since that is a separate code path in the library.

## FAQ

### Which StatsD servers does hot-shots support?

It is a client for Datadog's DogStatsD server, InfluxDB's Telegraf StatsD server, the OpenTelemetry Collector StatsD receiver and Etsy's StatsD server. It started as a fork of node-statsd and adds Unix domain socket and TCP protocol support, TypeScript types, Telegraf support, Datadog mode, events, child clients, debug logging and client-side aggregation.

### How do I configure the transport and the metric name in hot-shots?

Host, port, protocol, path and stream select the transport, with protocol taking tcp, uds or stream and defaulting to udp, and path used only for uds with a default of /var/run/datadog/dsd.socket. If no transport options are passed, the transport is configured from environment variables for parity with the official DogStatsD clients. The prefix and suffix options modify every metric name, and a period separator is added automatically if one is not present.

### What tags does hot-shots add automatically in Datadog mode?

With includeDataDogTags enabled, which is the default, the dd.internal.entity_id tag comes from DD_ENTITY_ID, env from DD_ENV, service from DD_SERVICE and version from DD_VERSION. Tags from DD_TAGS or its legacy alias DATADOG_TAGS are added as well, and if that value contains no comma then whitespace is used as the separator instead, matching dd-trace-js and the Datadog Agent. These general tags are applied before the DD_ENV, DD_SERVICE and DD_VERSION mapping.

## Sources

- [bdeitte/hot-shots on GitHub](https://github.com/bdeitte/hot-shots)
- [Issues](https://github.com/bdeitte/hot-shots/issues)
- [License: MIT](https://github.com/bdeitte/hot-shots/blob/main/LICENSE)
- [README](https://github.com/bdeitte/hot-shots/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/bdeitte-hot-shots
