Skip to content

Repository files navigation

IotHubThrottleTester

A C# console app that generates D2C (device-to-cloud) message load against an Azure IoT Hub, to deliberately exercise its rate-throttling and daily-quota limits for testing purposes.

It provisions its own simulated device identities, sends messages from them concurrently, tallies what happens (success / throttled / quota-exceeded / other), and deletes the simulated devices again when it's done.

How it works

  1. Provisions N simulated devices on your hub via RegistryManager, in batches of 100 (DeviceProvisioner.cs). Each gets its own client-generated symmetric key and a device ID like throttle-test-3f9a2b7c-00000 (see Device naming below).
  2. Opens a DeviceClient per device (MessageSender.cs), using AMQP with connection pooling by default so hundreds of devices share a handful of real TCP connections instead of one each.
  3. Sends messages from a pool of concurrent workers, round-robining across the simulated devices, until either the message-count target or a time limit is hit (or you cancel).
  4. Retries are disabled (NoRetry) on purpose — this tool exists to observe throttling and quota errors, not have the SDK silently swallow them via automatic retry.
  5. Deletes the simulated devices in a finally block, so cleanup runs even if the run is cancelled or crashes partway through (unless --keep-devices is passed).

Prerequisites

  • .NET 8 SDK
  • An Azure IoT Hub, and a connection string with RegistryWrite + ServiceConnect permissions (the built-in iothubowner shared access policy has both). Get it from: Azure Portal → your IoT Hub → Shared access policiesiothubownerPrimary connection string.

Running it

Set the connection string as an environment variable (PowerShell, current session only):

$env:IOTHUB_OWNER_CONNECTION_STRING = "HostName=<yourhub>.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=<key>"

Then build and run:

dotnet run -- --devices 50 --messages 450000

(Note the -- — needed so dotnet run passes the flags through to the app instead of parsing them itself. Not needed when running the built .exe directly.)

Or pass the connection string inline instead of using the environment variable (ends up in shell history, so prefer the environment variable for anything beyond a one-off):

.\IotHubThrottleTester.exe --connection-string "HostName=...;SharedAccessKeyName=iothubowner;SharedAccessKey=..." --devices 50 --messages 450000

CLI options

Flag Default Description
--connection-string <s> IOTHUB_OWNER_CONNECTION_STRING env var iothubowner (or equivalent) connection string
--devices <n> 50 number of simulated devices to provision
--messages <n> 450000 total messages to send across all devices
--message-size <bytes> 1024 payload size per message
--concurrency <n> 200 max in-flight sends at a time
--rate <n> 0 (unlimited) cap on messages/sec
--duration <seconds> 0 (unbounded) stop after this many seconds regardless of --messages
--device-prefix <s> throttle-test device ID prefix
--keep-devices off don't delete the provisioned devices when done
--mqtt off use MQTT instead of AMQP (AMQP with connection pooling is the default)

Run --help for the same summary from the command line.

Throttling vs. quota — two different limits, two different tests

Azure IoT Hub enforces two distinct limits that are easy to conflate but need different load shapes to exercise:

Throttling (HTTP 429) Quota exceeded (error 403002)
What it limits Operations per second Total messages per day
Behavior Transient — back off and it recovers Hard wall — every send fails until reset
Resets Immediately (next second/minute) UTC midnight
Azure Monitor metric "Number of throttling errors" No dedicated error-count metric — only the "Total number of messages used" usage gauge
SDK exception (this app) IotHubThrottledException DeviceMaximumQueueDepthExceededException — see note below

Because they need opposite load shapes, compose the flags differently for each:

Throttling burst — short, high-concurrency, unbounded rate, capped by time instead of count:

.\IotHubThrottleTester.exe --duration 20 --concurrency 500 --messages 100000000

Quota drain — long, steady, paced under the per-second limit so only the daily cap gets exercised (not rate-throttling), capped by message count:

.\IotHubThrottleTester.exe --messages 450000 --rate 150

Note: quota exhaustion is sticky for the rest of the UTC day. Once a run pushes the hub over its daily quota, every subsequent run (including a throttling burst test) will report Quota exceeded immediately for everything until the daily reset at UTC midnight — that's expected, not a bug.

A quirk in the classic SDK's exception types

The Microsoft.Azure.Devices.Client SDK never introduced a dedicated QuotaExceededException for D2C sends, because doing so would have been a breaking change. Instead, when a send is rejected for daily-quota-exceeded (error 403002), it throws DeviceMaximumQueueDepthExceededException — the inner exception message says so explicitly. This app catches that exception type and counts it as Quota exceeded, not Other errors; see Program.cs for the exact exception handling.

Reading the output

While running, a progress line is printed every 2 seconds:

[12:55:17] sent=16,284/450,000 ok=0 throttled=0 quota=0 other=16,284

At the end, a summary is printed:

=== Results ===
Stopped because  : message target reached
Elapsed          : 00:01:41
Sent (attempted) : 450,000
Succeeded        : 402,103
Throttled (429)  : 0
Quota exceeded   : 47,897
Other errors     : 0
Throughput       : 4,431.7 msg/s
  • Stopped becausemessage target reached, duration limit reached (Ns), or cancelled by user (Ctrl+C).
  • Succeeded / Throttled / Quota exceeded — mutually exclusive outcome per message.
  • Other errors — anything not matching a known throttling/quota exception; the first occurrence of each distinct exception type is printed live as [new error type] ..., and all are broken down by type in the summary, so a genuine bug or connectivity problem doesn't get silently lumped in as "some error happened."

Device naming

Device IDs follow {devicePrefix}-{runId}-{index:D5}, e.g. throttle-test-3f9a2b7c-00000 — a fresh 8-character runId per run, so devices from different runs don't collide, and the whole set can be found in the portal by filtering on the prefix.

Cleanup and safety

  • Simulated devices are deleted automatically after each run (finally block), including on Ctrl+C. Pass --keep-devices to skip this (e.g. to inspect them, or reuse them across runs).
  • If a run is killed outright (process killed, terminal closed, machine crash) rather than cancelled normally, cleanup won't run and devices will be left behind — check the portal for leftover throttle-test-* devices if that happens, and delete them manually or with RemoveDevices2Async via a script.
  • The daily quota this app is designed to exceed is a property of your whole hub, shared with any real devices/traffic on it. Don't point this at a production hub unless you intend to actually exhaust its daily message budget for everyone using it.

About

A C# console tool that generates device-to-cloud message load against an Azure IoT Hub to deliberately trigger its rate-throttling (429) and daily message quota (403002) limits. Provisions and tears down its own simulated device identities per run, with separate load shapes for burst-style throttling tests vs. sustained quota-drain tests.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages