mrkeyoor.com_
Thu 06 Aug 10:54 UTC
PyPIInfraupdated 06 Aug 2026

datadog

The PyPI package named datadog (its repository is datadogpy) is Datadog's older hand-written Python library, and it holds three separate tools that people regularly mistake for each other. datadog.statsd is a DogStatsD client that fires metrics, events and service checks at a Datadog Agent running beside your process over UDP or a Unix socket; this is the part nearly everyone actually wants. datadog.api is a REST client for the Datadog HTTP API covering monitors, dashboards, downtimes, SLOs and similar resources, but only the endpoints a human added by hand. datadog.threadstats aggregates metrics in the process and posts them over HTTP on a timer, for places where no Agent is reachable, such as AWS Lambda. There is also a dog command line tool built on the API client. A single initialize() call configures all of them, either from keyword arguments or from DD_API_KEY, DD_APP_KEY and DD_DOGSTATSD_URL in the environment.

Verdict

If a Datadog Agent is already running beside your service, this is the correct package for custom metrics and the DogStatsD client is the mature, well-tuned part of it. Treat datadog.api as a convenience for scripts rather than a real API client, and remember that a missing Agent produces empty dashboards rather than errors.

API stability5/5The statsd surface has been stable for years and changes arrive as new optional keyword arguments such as cardinality, timestamp and max_metric_samples_per_context; the package is still pre-1.0 by version number but in practice upgrading across the 0.5x line has not broken calling code, and the deprecated open_buffer and close_buffer pair still works alongside the context manager that replaced it
Docs3/5The README covers installation, both transports, origin detection, packet sizes and benchmarks, but it repeatedly hands you off to docs.datadoghq.com for the actual metric submission reference, so the parameter list for the thing you are calling lives on a different site; the Read the Docs build is a bare autodoc dump, and nothing documents the defaults that surprise people, namely buffering off, aggregation off and timing in seconds
Maintenance3/5Pushed 2026-07-30 and clearly staffed by Datadog with typing added across api, util, dogstatsd and dogshell in 0.52.2, but the release rhythm is poor: 0.52.1 in July 2025 then nothing until 0.52.2 in June 2026, and 49 genuinely open issues out of 83 open issues and PRs against a vendor-owned client suggests triage is not anyone's main job
Ecosystem4/5Around 15.4M downloads a week and the default way Python code emits custom metrics to Datadog, with the DogStatsD wire protocol shared across every other Datadog client; the mark comes off because Datadog splits its own Python story across three packages, and the newer generated API client makes datadog.api the legacy half of this one

Use it if

  • You run a Datadog Agent next to your application and want custom metrics: statsd.increment, gauge, histogram, distribution and timing write to a local socket and never block on the network, so instrumentation cannot take your request path down
  • You want latency instrumentation in one line: @statsd.timed('handler.latency') works as a decorator, a context manager, and on async coroutines, defaulting the metric name to module.function when you omit it
  • You are on Kubernetes and want pod-level tags for free: origin detection reads DD_ENTITY_ID, and DD_ENV, DD_SERVICE and DD_VERSION are appended to the client's constant tags automatically
  • You need application-level events and service checks, not only metrics: statsd.event() and statsd.service_check() go over the same DogStatsD socket with the same tags
  • You have cron jobs or scripts that touch the Datadog API for common resources: api.Monitor, api.Event, api.Metric and the bundled dog CLI cover the everyday cases without adding a large generated client
Skip it if

Setup reality

pip install datadog pulls one runtime dependency, requests, and installs an import named datadog even though the project is called datadogpy. That name is the first tax you pay, because Datadog ships three Python packages that overlap in nothing: datadog is this one, datadog-api-client is the generated full REST client, and ddtrace is APM and profiling. All three read DD_ prefixed environment variables and none is a superset of the others. The second tax is the Agent. DogStatsD does not send anything to Datadog, it sends to an Agent on 127.0.0.1:8125 or a Unix socket, so on a laptop with no Agent every metric disappears with no error at all; point DD_DOGSTATSD_URL at udp://host:8125 or unix:///var/run/datadog/dsd.socket, or set DD_DOGSTATSD_DISABLE=1 during development. Configuration precedence is worth learning once: explicit arguments to initialize() beat DD_DOGSTATSD_URL, which beats DD_AGENT_HOST and DD_DOGSTATSD_PORT. On the API side you need both DD_API_KEY and DD_APP_KEY, and any account outside the US1 site must also set api_host or DATADOG_HOST, since the default is https://api.datadoghq.com and an EU key against it fails as an auth error rather than a region error. Two further sharp edges: initialize() mutates the module level statsd singleton in place and appends to its constant_tags, so calling it twice duplicates every global tag; and statsd.timing and @statsd.timed record seconds by default because use_ms is False, which is not the unit most Datadog dashboards expect.

Patterns

Configure once, then use the shared clientinitialize-and-send

from datadog import initialize, statsd

# explicit
initialize(
    statsd_host="127.0.0.1",
    statsd_port=8125,
    statsd_namespace="checkout",
    statsd_constant_tags=["env:prod", "service:api"],
)

# or entirely from the environment:
#   DD_DOGSTATSD_URL=udp://localhost:8125
#   DD_AGENT_HOST=dd-agent  DD_DOGSTATSD_PORT=8125
#   DD_ENV=prod  DD_SERVICE=api  DD_VERSION=1.4.0
initialize()

statsd.increment("orders.created")   # -> checkout.orders.created

initialize() mutates the module-level statsd object in place, so importing statsd before or after the call gives you the same client. It also does statsd.constant_tags += statsd_constant_tags, meaning a second initialize() in the same process duplicates every global tag. Call it exactly once, at startup, before you fork workers.

Pick the right metric typemetric-types

from datadog import statsd

statsd.increment("queue.jobs.enqueued")                 # count
statsd.decrement("queue.depth")
statsd.count("bytes.uploaded", 4096)

statsd.gauge("queue.depth", 42)                          # last value wins
statsd.set("users.active", user_id)                      # unique count

statsd.histogram("payload.bytes", 1024)                  # percentiles, Agent-side
statsd.distribution("request.latency", 0.180)            # percentiles, global

# backfill a past minute (counts and gauges only)
statsd.count_with_timestamp("orders.created", 3, timestamp=1_754_400_000)

histogram aggregates on the Agent, so percentiles are per host and cannot be recombined across a fleet; distribution ships raw points and computes percentiles server-side, which is what you want for latency in an autoscaled service and which costs more. gauge is last-write-wins per flush interval, so two processes writing the same gauge name without distinguishing tags will overwrite each other.

Measure latency with the decorator or context managertime-code

from datadog import statsd

@statsd.timed("checkout.charge.latency", tags=["gateway:stripe"], use_ms=True)
def charge(order):
    ...

@statsd.timed(use_ms=True)          # name defaults to module.function
async def fetch_user(uid):
    ...

with statsd.timed("report.build", use_ms=True) as timer:
    build_report()
print(timer.elapsed)                 # milliseconds, because use_ms=True

# same shape, but emits a distribution instead of a timing
@statsd.distributed("checkout.charge.latency", use_ms=True)
def charge_v2(order):
    ...

The default unit is seconds, because DogStatsd is constructed with use_ms=False, and most Datadog dashboards and monitors are written expecting milliseconds. Set use_ms=True per call or once on the client and be consistent, because mixing units in one metric name produces a graph that looks like a thousandfold regression. The timer records on the way out of a raised exception too, so slow failures still show up.

Tag metrics and sample the noisy onestags-and-sampling

from datadog import statsd

statsd.increment(
    "http.requests",
    tags=[f"route:{route}", f"status:{code}", f"method:{method}"],
)

# emit 10% of the points, values scaled back up by the Agent
statsd.increment("cache.lookup", sample_rate=0.1)

# global tags added to everything from this client
statsd.constant_tags.append("region:ap-south-1")

Never put an unbounded value such as a user id, request id or full URL in a tag: every distinct combination is a separate custom metric on your bill, and Datadog will start dropping the tag once the cardinality limit is hit. sample_rate only helps counts and timings, where the Agent can scale the value back up; sampling a gauge just means you sometimes send nothing.

Stop paying one syscall per metricbatch-metrics

from datadog import statsd

# one packet for the whole block
with statsd:
    for row in rows:
        statsd.increment("rows.processed", tags=[f"table:{row.table}"])

# or turn on client-side aggregation for the process lifetime
statsd.enable_aggregation(flush_interval=2.0)

# and, for high throughput, hand sending to a background thread
statsd.enable_background_sender(sender_queue_size=10_000)
...
statsd.stop()      # flush and join before the process exits

Buffering and aggregation are both disabled by default, so out of the box each call is a separate write to the socket. The context manager form buffers and flushes on exit; enable_aggregation combines identical metric-and-tag pairs before sending. If you enable the background sender you own the shutdown: without statsd.stop(), whatever is still queued is lost, which hits short scripts and Kubernetes jobs hardest.

Use a Unix socket instead of UDPunix-socket-transport

from datadog import initialize, statsd

initialize(statsd_socket_path="/var/run/datadog/dsd.socket")

# equivalent, and usually set by the deployment rather than the code:
#   DD_DOGSTATSD_URL=unix:///var/run/datadog/dsd.socket

# larger default packet on UDS (8192) than UDP (1432)
initialize(statsd_socket_path="/var/run/datadog/dsd.socket", max_buffer_len=8192)

UDS avoids the UDP packet loss that shows up as mysteriously missing points under load, and it lets the Agent identify the sending container without DD_ENTITY_ID. It also means a real error when the Agent is down, since the socket connect fails, so the client retries the connection rather than writing into nothing. Mount the socket into the container and make sure the app user can write to it.

Send an event and a service checkevents-and-service-checks

from datadog import statsd

statsd.event(
    title="Deploy finished",
    message="api %s rolled out to prod" % version,
    alert_type="success",          # error | warning | info | success
    aggregation_key="deploy:api",
    tags=["env:prod", "service:api"],
)

statsd.service_check(
    check_name="api.db.can_connect",
    status=0,                       # 0 OK, 1 WARNING, 2 CRITICAL, 3 UNKNOWN
    message="primary reachable",
    tags=["db:primary"],
)

Both go over the same DogStatsD socket, so both are fire and forget and both are subject to the packet size limit; a long event message is silently truncated or dropped rather than rejected. aggregation_key is what groups repeated events into one item in the event stream, so set it or a noisy deploy job floods the timeline.

Talk to the Datadog HTTP API from a scriptrest-api-calls

from datadog import initialize, api

initialize(
    api_key=os.environ["DD_API_KEY"],
    app_key=os.environ["DD_APP_KEY"],
    api_host="https://api.datadoghq.eu",   # US1 is the default
)

api.Event.create(
    title="Nightly import complete",
    text="imported %d rows" % n,
    tags=["job:import"],
)

api.Monitor.mute(monitor_id, end=int(time.time()) + 3600)
api.Metric.send(metric="batch.rows", points=n, tags=["job:import"])

The API side needs both keys, unlike DogStatsD which needs neither. If your organisation is on the EU, US3, US5 or AP1 site and you leave api_host alone, every call goes to api.datadoghq.com and comes back as an authentication failure rather than a wrong-region message, which is a wasted afternoon. Coverage here is whatever someone hand-wrote; check datadog-api-client before assuming an endpoint exists.

Find out why nothing reaches Datadogdebug-missing-metrics

import logging
from datadog import statsd

# the library attaches a NullHandler, so turn its logger on first
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("datadog.dogstatsd").setLevel(logging.DEBUG)

statsd.increment("probe")
print(statsd.packets_dropped, statsd.bytes_dropped)

# confirm the Agent is actually listening
#   echo -n 'probe:1|c' | nc -u -w1 127.0.0.1 8125
#   sudo datadog-agent status | grep -A5 dogstatsd

# in development, stop pretending to send at all
#   DD_DOGSTATSD_DISABLE=1

This is the routine to memorise, because the failure has no signal by default: datadog.dogstatsd ships with a NullHandler, most send errors are logged at debug, and a UDP write into nothing succeeds. packets_dropped and bytes_dropped come from the client's own telemetry, which is on by default and does not count against your custom metric bill.

Build your own client instead of the global oneown-client-instance

from datadog.dogstatsd import DogStatsd

metrics = DogStatsd(
    host="dd-agent",
    port=8125,
    namespace="worker",
    constant_tags=["queue:emails"],
    disable_buffering=False,
    use_ms=True,
)

metrics.increment("jobs.done")

# in tests
null = DogStatsd(disable_telemetry=True, track_instance=False)

Libraries should never call initialize(), because it reconfigures the singleton the host application owns; construct a DogStatsd instance instead. Separate instances also let one component batch aggressively while another sends immediately. Pass track_instance=False for throwaway clients so they stay out of the global weak set used for fork handling.

Survive gunicorn workers and process exitfork-and-shutdown

from datadog import initialize, statsd

# gunicorn.conf.py
def post_fork(server, worker):
    initialize()                     # per-worker configuration
    statsd.enable_background_sender()

def worker_exit(server, worker):
    statsd.stop()                    # flush, join, close the socket

# a plain script
import atexit
atexit.register(statsd.stop)

On any interpreter with os.register_at_fork the client hooks fork itself, flushing and restarting its threads on both sides, so a socket is not shared between parent and children; set DD_DOGSTATSD_DISABLE_FORK_SUPPORT to turn that off only if it conflicts with your own handlers. Shutdown is still manual: stop() flushes aggregated and buffered metrics and joins the sender queue, and the client stays usable afterwards.

Report without an Agent, including Lambdaagentless-metrics

from datadog import initialize, ThreadStats

initialize(api_key=os.environ["DD_API_KEY"])

stats = ThreadStats()
stats.start(flush_interval=10)          # background thread posts over HTTPS
stats.increment("batch.rows", 100, tags=["job:import"])
stats.flush()

# AWS Lambda
from datadog import datadog_lambda_wrapper, lambda_metric

@datadog_lambda_wrapper
def handler(event, context):
    lambda_metric("lambda.invocations", 1, tags=["fn:importer"])

ThreadStats posts straight to the API, so it needs an API key and it does add network latency and failure modes to your process, which is exactly what DogStatsD exists to avoid. Use it only where no Agent can run. For Lambda specifically, Datadog now recommends the separate datadog-lambda package and its extension layer; the wrapper here is the older path and still flushes on the same invocation.

Alternatives

PackageRegistryPick it when
datadog-api-clientPyPIYou need complete and current coverage of the Datadog REST API, including v2 endpoints that datadog.api never grew methods for
ddtracePyPIYou want distributed traces, profiling and automatic framework instrumentation rather than metrics you write by hand
opentelemetry-sdkPyPIYou want vendor-neutral instrumentation you can route to Datadog now and somewhere else later without touching application code
statsdPyPIYour target is a plain StatsD server with no tags, events or service checks and you want the smallest possible client