Alright, let’s talk about getting your own data into CloudWatch. The built-in metrics are great for a quick look, but the moment you need to track something specific to your business—like “number of times a user uploaded a cat picture that was actually a dog,” or “internal queue backlog depth”—you’re in the land of custom metrics. This is where you graduate from watching your cloud to actually instrumenting it.

The workhorse here is the PutMetricData API. Don’t let the name fool you; it’s less about “putting” a single data point and more about publishing a batch of them efficiently. You’ll use this through the AWS CLI or an SDK. I almost always recommend the SDK for anything in production—it’s more robust, you get proper error handling, and you can bake it right into your application logic.

The Nuts and Bolts of a PutMetricData Call

Every time you call PutMetricData, you’re sending a request to the CloudWatch API with a payload containing one or more metrics. Each metric needs a few key pieces of information to be useful:

  • Namespace: This is your top-level category. It’s a string, like MyAwesomeApplication/Production. Use it to group all metrics for a specific service or application. Pro tip: make this meaningful. App01 tells you nothing; BillingService/TransactionProcessor does.
  • MetricName: The name of the thing you’re measuring, like UserLogins or DatabaseQueryLatency.
  • Value: The numerical value of the measurement. This is a double, so you can send integers or floating-point numbers.
  • Unit: This is often overlooked but crucially important. Is the value in Bytes, Seconds, Count, or Percent? Specifying this correctly is what lets CloudWatch do smart things like automatically convert bytes to megabytes on your dashboards. If you don’t know, use None. Don’t just leave it out—the default might not be what you think.
  • Dimensions: These are your key-value pairs for slicing and dicing the metric. Think of them as tags. For a web application, you might have a dimension like Environment=Production or APIEndpoint=/user/create. This is how you answer questions like “What’s the latency just for the login endpoint in staging?”

Here’s the kicker: the combination of Namespace, MetricName, and Dimensions uniquely defines a metric. Send a value with a new combination of dimensions, and CloudWatch will quietly create a brand new metric for you. It’s powerful, but it can also lead to metric explosion (and a surprising bill) if you’re careless, like using a user ID as a dimension value. Don’t do that.

Publishing via the AWS CLI

The CLI is perfect for quick tests or shell scripts. The syntax is… well, it’s a bit JSON-heavy, but you get used to it.

aws cloudwatch put-metric-data \
  --namespace "MyApp/Test" \
  --metric-data \
  '[
    {
      "MetricName": "OrdersProcessed",
      "Value": 42,
      "Unit": "Count",
      "Dimensions": [
        {
          "Name": "Environment",
          "Value": "Development"
        },
        {
          "Name": "Process",
          "Value": "WorkerNode-1A"
        }
      ]
    }
  ]'

Run that, wait a minute or two (data isn’t instantaneous), and then check your CloudWatch console. Your custom metric should be there, proudly doing nothing until you send more data.

The Right Way: Using the SDK (Python Example)

For any real application, you’ll use an SDK. Here’s how you do it in Python (Boto3). Notice how we’re sending two different metrics in a single call—this is the efficient way to do it.

import boto3
from datetime import datetime

client = boto3.client('cloudwatch')

# Create the metric data list
metric_data = [
    {
        'MetricName': 'PageLoadTime',
        'Timestamp': datetime.now(),
        'Value': 755.2,
        'Unit': 'Milliseconds',
        'Dimensions': [
            {
                'Name': 'PageName',
                'Value': 'Homepage'
            },
            {
                'Name': 'Environment',
                'Value': 'Production'
            }
        ]
    },
    {
        'MetricName': 'SuccessfulLogins',
        'Timestamp': datetime.now(),
        'Value': 1,
        'Unit': 'Count',
        'Dimensions': [
            {
                'Name': 'AuthenticationMethod',
                'Value': 'OAuth'
            }
        ]
    }
]

# Send the batch of metrics
response = client.put_metric_data(
    Namespace='MyWebApp/Performance',
    MetricData=metric_data
)

print("Metrics published successfully:", response)

Pitfalls, Sharp Edges, and Best Practices

  1. Resolution: Standard vs. High. By default, metrics are stored at standard resolution (1-minute granularity). You can opt for high resolution (1-second) by including a StorageResolution parameter. But be warned: high-resolution metrics cost more. Only use it if you genuinely need to alarm or graph on a sub-minute basis.
  2. Timestamps are Your Responsibility. The SDK will stamp the data with the current time if you don’t provide a Timestamp. This is fine for near-real-time data. But if you’re batch-processing historical logs and publishing metrics later, you must provide the correct historical timestamp. CloudWatch will accept data up to two weeks in the past (and 2 hours in the future, somehow).
  3. Batching is Non-Optional. Every PutMetricData call has overhead. Never, ever call it for a single data point in a loop. Always batch them up. The maximum batch size is 20 metrics per call and 1 MB per request. Your goal is to minimize the number of API calls, not just because it’s slower, but because those calls themselves… you guessed it… cost money.
  4. Embrace Statistics, Not Just Data Points. You can publish a pre-aggregated set of values using the StatisticValues parameter (Count, Sum, Minimum, Maximum). This is a huge win if you’re calculating these stats anyway—like if you’re measuring latency for 1000 requests. Instead of sending 1000 data points, you send one metric with {Count:1000, Sum:45000, Min:20, Max:2000}. It’s cheaper, faster, and just as effective for graphing.