---
title: "Meter Only Successful API Responses, Not Errors"
description: "Your gateway is counting every 500, timeout, and retry against your customers' quotas. Here's how Zuplo does it with a single line of config, and why most gateways make you write custom policy code to get the same behaviour."
canonicalUrl: "https://zuplo.com/blog/2026/04/15/stop-paying-for-500s"
pageType: "blog"
date: "2026-04-15"
authors: "martyn"
tags: "API Monetization, API Gateway"
image: "https://zuplo.com/og?text=Meter%20Only%20Successful%20API%20Responses"
---
Your API has a bad afternoon. A downstream service flaps, 3% of requests return
500s for an hour, and your customers retry with exponential backoff. Every retry
is a fresh inbound request that hits your gateway, so by end of day they've
burned half their monthly quota on errors _you_ caused. The next morning, your
support inbox is full of "why am I rate-limited when none of my requests
worked?"

This is a metering bug, and on most gateways it's the default behaviour.

<CalloutAudience
  variant="useIf"
  items={[
    `You bill API customers based on request count or usage tiers`,
    `Your gateway meters requests before the response status is known`,
    `You've had a customer ask why their quota burned during an outage they didn't cause`,
  ]}
/>

## Why Your Gateway Meters Errors

Most metering happens on the request, not the response. The gateway sees the
request, increments the counter, and forwards to your backend. 500? Timeout?
Doesn't matter, the counter already moved.

That's fine for capacity planning. It's wrong if you're billing customers on it.
Customers pay for successful calls. A 500 is not a successful call.

To bill correctly you need metering to fire _after_ the response, and only when
the response was useful to the caller.

## How Zuplo Does It

Zuplo's `monetization-inbound` policy has a `meterOnStatusCodes` option that
controls which responses count. Add `meterOnStatusCodes: "200-299"` to your
policy options (in `config/policies.json` or via the UI), and only successful
responses get metered. No custom logic, no post-response hooks, no extra wiring.
One line:

```json
{
  "name": "monetization-inbound-policy",
  "policyType": "monetization-inbound",
  "handler": {
    "export": "MonetizationInboundPolicy",
    "module": "$import(@zuplo/runtime)",
    "options": {
      "meters": {
        "api_requests": 1
      },
      "meterOnStatusCodes": "200-299"
    }
  }
}
```

The `meters` object maps a meter key (`api_requests`) to the increment per
qualifying response, and the `meterOnStatusCodes` range decides what counts as
qualifying. With that one line in your policy, 500s don't meter, 4xx errors
don't meter, and retries on a broken backend don't eat the customer's quota.

If you've ever widened the range, change it back.

<CalloutTip variant="mistake">
  The most common way this gets widened is during debugging. Someone is
  troubleshooting a "why isn't my meter firing?" issue, opens the range up to
  confirm the meter is being called at all, and then forgets to put it back.
  Worth a quick audit of your <code>meterOnStatusCodes</code> setting if you've
  ever debugged a meter in production.
</CalloutTip>

You can also pass explicit codes if you want tighter control:

```json
{
  "options": {
    "meters": { "api_requests": 1 },
    "meterOnStatusCodes": "200, 201, 202, 204"
  }
}
```

## Edge Cases Worth Thinking About

**429 rate limits.** Don't meter. The customer hit a limit, got no value, and
metering a 429 on top of rate-limiting is double jeopardy. Setting
`meterOnStatusCodes: "200-299"` handles this because 429 is in the 4xx range.

**401 and 403.** Don't meter. The request was rejected before it reached your
business logic. The same `"200-299"` setting covers this.

**207 Multi-Status.** Genuinely ambiguous, and only you know the answer for your
API. If a batch call returned 207 with 6 successes and 4 errors, did the
customer get value? Probably, but maybe not the value they paid for. Consider
metering 207s with a meter that reflects actual successful sub-operations, not
one flat tick per request.

**Streaming responses that die mid-flight.** Status 200 is sent before the
stream starts. If it fails halfway, you've already metered. For streaming
responses, meter on what you can measure at the end: tokens returned, bytes
sent, records streamed. See the
[meters documentation](https://zuplo.com/docs/articles/monetization/meters) for
how to configure meters on extracted event values rather than a flat increment.

## What Other Gateways Make You Do

On most gateways, you get there with custom code, whether that's a post-request
hook, a Lambda, or a policy that runs after the response, pulls the status, and
conditionally fires an increment against your billing backend. You write it,
test it, maintain it, and debug it the next time someone adds an endpoint and
forgets to wire it up.

None of that is hard, but it's all work nobody asked for. A gateway that bills
your customers for your own 500s, and where the documented fix is "write a
custom policy", is a gateway problem, not a you problem.

## Before and After

Before: customer retries through your 500, burns quota, opens a ticket.

After: the request fails, the meter doesn't move, the customer never knows.

That second outcome is what Zuplo's `monetization-inbound` policy gives you with
one line of config: `meterOnStatusCodes: "200-299"`. If you're already on Zuplo,
that's the only line you need to add (or confirm is still in place, if someone
widened it during a debugging session and forgot to roll it back). If you're on
something else, you've got a custom policy to write and probably a billing
report to apologise for.

<CalloutDoc
  title="Monetization Policy Reference"
  description="Full reference for the monetization-inbound policy: every meterOnStatusCodes format, the complete options surface, and the underlying meter and feature model."
  href="https://zuplo.com/docs/articles/monetization/monetization-policy"
  icon="book"
/>