Skill 61 · AWS Marketplace Metering
Subchapter 61.15
references/troubleshooting.mdMarkdown19 KBView on GitHub
The entity ID (prod-xxx) is not the metering product code. Check ResolveCustomer response in Lambda logs for the actual product code, then update ProductCode parameter in template.yaml and redeploy.
The dimension key doesn’t match what’s configured on the product. Verify dimensions in AMMP or via the Catalog API. Add missing dimensions in AMMP before retrying.
The metering API is regional and supported in MOST — but not all — commercial regions. If a call returns DisabledApiException, the chosen region does not yet support the API (e.g. a newly launched region). This is NOT a code or IAM bug: meter in a supported region instead (see the region-support list (opens in a new tab)). The metering region is the seller’s choice (the region where usage occurs, or the nearest supported region), and per-region dedup makes metering the same usage in multiple supported regions correct — there is no InvalidEndpointRegionException for a “wrong” supported region.
Metering is driven by the usage table’s metering_pending GSI, not by subscriber status.
Check:
meteringPending (the %Y-%m-%dT%H hour bucket) on rows as
it writes them? The meter Lambda only meters rows that appear on that sparse GSI.now-23h … now-1h
window and never the in-progress current hour.License Updated event (not Purchase Agreement Created) establishes/refreshes it. Check the subscription Lambda logs in us-east-1 and
the DLQ for failed processing. A missing subscriber row does NOT block metering (the
CA UsageRecord is built from the usage rows).Marketplace events emit ONLY in us-east-1. Ensure the events stack is deployed to us-east-1, not the seller’s preferred region.
Important: EventBridge does NOT retroactively deliver past events. If you deploy the events stack late, any subscriptions that occurred before deployment are lost. For those customers, manually query their subscription status or have them re-click “Set up your account” to trigger ResolveCustomer.
Cross-region pattern (only when the metering region is NOT us-east-1): If your main metering stack is in a different region (e.g., eu-west-1), the Meter/Register Lambdas read the us-east-1 subscribers table cross-region (already handled by SUBSCRIBERS_TABLE_REGION=us-east-1). If your metering region IS us-east-1, no cross-region setup is needed — deploy both the events stack and the main stack in us-east-1. us-east-1 is a supported BatchMeterUsage region.
The registration token is reusable until it expires (~4 hours) — it is NOT one-time-use, so a buyer who hit an error on the registration page can retry with their ORIGINAL link as long as they are still within that ~4-hour window (re-resolving the same token returns the same result). Once the ~4h has elapsed the token is expired: ResolveCustomer returns ExpiredTokenException, and the buyer must re-click “Set up your account” from the AWS Marketplace console to mint a FRESH token (a full resubscribe is NOT required). A token that was somehow already consumed/invalidated returns InvalidTokenException — same fix. So if a buyer clicked “Set up your account” days ago and only retries now, the original link’s token is long expired and will NOT resolve — have them click “Set up your account” again for a new token, then retry.
Root cause: IAM namespace mismatch. ResolveCustomer uses aws-marketplace:ResolveCustomer — this is a DIFFERENT service namespace from meteringmarketplace:BatchMeterUsage. Granting marketplace:* or meteringmarketplace:* does NOT cover ResolveCustomer.
The fix: Your Lambda role needs BOTH actions from DIFFERENT namespaces:
aws-marketplace:ResolveCustomer — for the registration flowaws-marketplace:BatchMeterUsage — for metering (same namespace as ResolveCustomer, NOT meteringmarketplace)Common mistake: Seller grants meteringmarketplace:* thinking it covers all Marketplace APIs. It doesn’t — ResolveCustomer is under aws-marketplace:, not meteringmarketplace:.
Note: Region — ResolveCustomer is regional but the region is the SELLER’S CHOICE; it does NOT have to match where the buyer uses the software or where
BatchMeterUsageis called, and it does NOT gate metering. It is NOT us-east-1-only. The real constraint is the ACCOUNT (must be the publishing account — see below). If you get AccessDeniedException, the issue is almost always the IAM namespace (aws-marketplace:).
When ResolveCustomer succeeds, it returns four fields — store ALL of them:
CustomerAWSAccountId — the buyer’s AWS account ID (use this for metering)LicenseArn — the license ARN for this agreement (use this for metering)CustomerIdentifier — opaque string (store for reference, NOT for metering)ProductCode — your metering product code (store for validation)The submitter retries the unprocessed records automatically ONCE. If any are STILL unprocessed after that retry, it does NOT raise or DLQ them — it logs an error, emits the UsageRecordUnprocessed metric, and leaves the records pending for the next submitter run (rate(5m)), so they are retried, not lost. Common causes: throttling, timestamp out of bounds (older than 24 hours), or a transient service issue. Because the submitter does not raise, this does NOT trip the Lambda Errors alarm — the dedicated *-usage-unprocessed alarm (on the UsageRecordUnprocessed metric) is the signal that records are repeatedly failing to meter. A persistent breach means the records are not clearing on retry; investigate the cause (and confirm they are not aging past the 24h window, which would then also trip UsageSubmissionExpired).
If usage occurred while the subscription was active, the metering record SHOULD be accepted. However, there’s a timing window:
Purchase Agreement Ended event was processed BEFORE your BatchMeterUsage call → you may get CustomerNotSubscribedOn contract renewal, a License Updated event fires via EventBridge (NOT Purchase Agreement Created and NOT a fabricated “Purchase Agreement Renewed” — no such event exists). The existing LicenseArn does NOT change on renewal — it persists across contract periods. The same CustomerAWSAccountId + LicenseArn you’ve been using continue to work. No metering code changes needed.
Key facts about renewals:
License Updated event (not a new agreement event)RegisterUsage (NOT BatchMeterUsage) is for flat-rate subscription products (AMI, Container, or SaaS with fixed monthly fee):
Success if active, CustomerNotSubscribed if expiredThrottlingException if called too frequently (back off and retry)InvalidProductCode usually means you’re passing the entity ID (prod-xxx) instead of the metering product code. To find the correct code:
aws marketplace-catalog describe-entity --catalog AWSMarketplace --entity-id <prod-xxx> --region us-east-1Look for the ProductCode field in the response (format varies, often alphanumeric like 4ml54db8vrmjuykaw1psroool). The code is case-sensitive.
The timestamp in each UsageRecord must be within the last 24 hours.
If you missed an hour:
ResolveCustomer must be called from the same AWS account that published the product. If your registration page is deployed in a different account, the token will not resolve — no buyer can onboard. This is not an IAM permission issue; it’s an account-level restriction.
BatchMeterUsage validates ALL records in a batch before accepting any. If even ONE record has a timestamp older than 24 hours, the ENTIRE batch is rejected with TimestampOutOfBoundsException. Always validate timestamps before sending. Remove invalid records and resubmit the rest.
Do NOT call GetEntitlements immediately after ResolveCustomer. A resolved token only means a customer identity was created — the agreement may not exist yet (it can happen before the agreement is finalized). Call GetEntitlements when you receive the License Updated EventBridge event instead.
The wind-down spans two events, and the two subscriber status fields move independently:
Purchase Agreement Ended — set agreementStatus = inactive ONLY. Do NOT change
subscriptionStatus, stop metering, or flush; the buyer may still be entitled until the
license is deprovisioned.License Deprovisioned (legacy SNS unsubscribe-pending) — set
subscriptionStatus = deprovisioning + a deprovisioningExpiry (event time + ~1h). This OPENS
the ~1-hour final-usage flush window: submit any remaining un-sent usage IMMEDIATELY. The
pipeline meters this row during the window; the events-stack deprovision-cleanup Lambda
(rate(15m)) then sets subscriptionStatus = inactive once deprovisioningExpiry has passed
(time-based finalization — NOT the submitter).
Once the server-side window closes, BatchMeterUsage returns CustomerNotSubscribed.License Deprovisioned, not Purchase Agreement Ended, opens
the flush window) and do NOT discard queued records before flushing. There is no
deprovisioned status value. Do NOT rely on a grace period beyond that ~1-hour window.The main stack’s UsageTable uses DeletionPolicy: Retain / UpdateReplacePolicy: Retain (intentional — never lose billing usage). If a first deploy fails and rolls back, the usage table is retained and left behind, so the next deploy’s dynamodb:CreateTable collides with the existing table and fails with ResourceInUseException. On a failed first deploy the retained <prefix>-usage table is empty — delete it, then re-deploy:
aws dynamodb delete-table --table-name <prefix>-usage --region <metering-region>Only do this for a failed/rolled-back initial deploy. Never delete a usage table that has already recorded billable usage.
The deployed stack creates awsmp-* CloudWatch metrics/alarms in the SELLER’s account for
the revenue-loss signals: per-stage Lambda Errors and Throttles (discoverer/aggregator/cleanup/submitter/expiry), a “meter did not run”
(low Invocations) alarm, two >24h usage age-out alarms by stage (EMF UsageAggregationExpired for raw usage never aggregated, UsageSubmissionExpired for aggregated records never submitted),
plus register/subscription-error and DLQ-depth alarms on the events stack. Business-status
metrics (aged-out, rejected, UsageRecordUnprocessed, CustomerNotSubscribed, DuplicateRecord, BatchMeterUsageException)
are emitted via CloudWatch Embedded Metric Format (EMF) from the pipeline Lambda logs — no extra
runtime IAM.
Alarms that tell you records are being rejected / not metered (each fires into AlertsTopicArn):
*-usage-unprocessed (UsageRecordUnprocessed) — records still unprocessed after the submitter’s one retry (the ONLY alarm-able signal for the UnprocessedRecords case; does not trip the Errors alarm).
*-usage-rejected (UsageRecordRejected) — client-side guardrail rejections (summed across all reasons).
*-batchmeterusage-exception (BatchMeterUsageException) — request-level exception failed a whole call (dimension/license/etc.).
*-customer-not-subscribed (CustomerNotSubscribed) — per-record status, no active subscription.
The >24h age-out alarms (UsageAggregationExpired/UsageSubmissionExpired) catch usage that silently never metered. DuplicateRecord is dashboard-only (informational), not an alarm.
Alarms are ALWAYS created. Each alarm’s actions wire to an OPTIONAL seller-supplied
in-region SNS topic (AlertsTopicArn on each stack); a CloudWatch alarm can only notify a
topic in its own region, so use a us-east-1 topic for the events stack and a
metering-region topic for the main stack.
Neither the deployer role nor any Lambda publishes to the topic — CloudWatch fires the action. Handling alarms and acting on failures is the seller’s responsibility; AWS Marketplace and this skill do not track client-side (seller-account) errors.
Thresholds, the SNS topic, retention, and KMS keys are seller customization points (the
TODO: parameters in the templates).
The packaged meter Lambda and usage-table schema describe the skill’s OWN new Concurrent
Agreements integration. For a legacy ProductCode-based integration (no LicenseArn), do
NOT assume that schema or any specific storage: how usage is stored, where
ProductCode/CustomerIdentifier come from, how subscriptions are tracked, and how the
final-hour flush is handled all depend on the seller’s EXISTING stack. First understand the
seller’s integration (ask questions, read their code/config) and ground legacy guidance in
that plus the public AWS documentation. Preserve only the API-level invariants (call
BatchMeterUsage with ProductCode at the request level and CustomerIdentifier in
UsageRecords; the 24h window and per-region dedup; never silently skip customers). Do NOT
impose the CA usage-table keys, the metering_pending GSI, registeredRegions, or the
agreementStatus/subscriptionStatus fields on a legacy stack unless it already uses them.
When usage is rejected BEFORE it is sent to BatchMeterUsage, the meter Lambda records a
terminal RejectedClientSide status plus a specific reason on the usage row, and emits the
UsageRecordRejected CloudWatch metric (dimensioned by Reason + ProductCode, watched by
the awsmp-*-usage-rejected alarm). A rejected row was never sent, so its dedup grain is
untouched — fix the source row and re-write it within 24h and it meters normally. The
reason codes and what to fix:
For the COMPLETE inventory of client-side guardrails — not just these reason codes, but also the timing/window, batch-shaping, and idempotency guardrails applied before
BatchMeterUsage— seereferences/client-side-guardrails.md.
| Reason | What it means / what to fix |
|---|---|
NegativeQuantity | quantity is negative. Write non-negative integers. |
NonIntegerQuantity | quantity is fractional/decimal (e.g. 2.5). Aggregate/round in your writer to an integer before persisting — it is NOT truncated for you. |
NonNumericQuantity | quantity is not a number. |
MissingDimension | The row has no dimension. |
TimestampInFuture / TimestampOutOfWindow | The metering hour is in the future or older than the 24h acceptance window. |
MalformedTimestamp | The raw-usage sort-key suffix is not an exact second-precision YYYY-MM-DDTHH:MM:SS timestamp (e.g. millisecond/fractional or hour-truncated). Write the suffix at whole-second precision. |
MalformedSortKey | The sort key is missing or has fewer than the three customerAWSAccountId#dimension#timestamp segments. |
SortKeyMismatch | The sort key’s account/dimension segments don’t match the row’s own customerAWSAccountId/dimension attributes (corrupt/mis-keyed row). |
MissingCustomerAWSAccountId | CA row without customerAWSAccountId. |
MissingLicenseArn | CA path without a real licenseArn. |
InvalidLicenseArn | licenseArn is not a syntactically valid License Manager ARN. Rejected client-side so it never poisons a batch. |
MissingLegacyIdentifier | Legacy row with neither CustomerIdentifier nor CustomerAWSAccountId. |
MixedAllocatedAndUnallocated | Within one (license, account, dimension, hour) group, some non-zero rows carry usageAllocations and some don’t. Make the group all-tagged or all-untagged. |
AllocationSumMismatch | The merged AllocatedUsageQuantity sum ≠ the record Quantity. Ensure each row’s allocations sum to that row’s quantity. |
TooManyTags | An allocation has more than 5 tags. |
TooManyAllocations | The merged group produced more than 2500 distinct tag-set allocations for one record. Reduce the tag cardinality per hour. |
MissingTagKeyOrValue / MalformedAllocations | A tag lacks a non-empty Key/Value, or usageAllocations is not the expected list-of-objects shape. |
A malformed/never-valid LicenseArn is returned by BatchMeterUsage as a request-level
InvalidLicenseException (not a per-record status). The meter Lambda validates the
LicenseArn shape client-side (rejecting InvalidLicenseArn before batching) and, if a
request-level error still occurs, ISOLATES the offending record by bisecting the batch
(bounded to ⌈log2(batch size)⌉ = 5 levels; at the cap a still-failing sub-batch is left pending for the next run),
so the other co-batched customers are still metered. A true CustomerNotSubscribed (valid
LicenseArn, ended/expired agreement, or suspended buyer) is a per-record status handled
terminally — do not retry it.