ButlerSDK.ApiRateLimiter 1.2.5-preview

This is a prerelease version of ButlerSDK.ApiRateLimiter.
dotnet add package ButlerSDK.ApiRateLimiter --version 1.2.5-preview
                    
NuGet\Install-Package ButlerSDK.ApiRateLimiter -Version 1.2.5-preview
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ButlerSDK.ApiRateLimiter" Version="1.2.5-preview" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ButlerSDK.ApiRateLimiter" Version="1.2.5-preview" />
                    
Directory.Packages.props
<PackageReference Include="ButlerSDK.ApiRateLimiter" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ButlerSDK.ApiRateLimiter --version 1.2.5-preview
                    
#r "nuget: ButlerSDK.ApiRateLimiter, 1.2.5-preview"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ButlerSDK.ApiRateLimiter@1.2.5-preview
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ButlerSDK.ApiRateLimiter&version=1.2.5-preview&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=ButlerSDK.ApiRateLimiter&version=1.2.5-preview&prerelease
                    
Install as a Cake Tool

ButlerSDK - Preview 1 - component of ButlerSDK - ApiKeyRateLimiter

ButlerSDK.ApiKeyRateLimiter

ApiKeyRateLimiter provides a lightweight, thread-safe mechanism for controlling access to services using two complementary forms of limits:

  • Per-service inventory — limits how many calls a service may make.
  • Shared budget — limits the total cost consumed across services.

A service can use either limit independently, both at the same time, or no limiting at all.

The limiter is intended for scenarios where an application needs to enforce usage boundaries around APIs, tools, services, or other metered operations without giving the limiter responsibility for actually executing those operations.

How It Works

Each service is registered with:

  • A service name
  • A cost per call
  • Its current inventory
  • Its maximum/reset inventory
  • A ButlerApiLimitType describing which limits apply

The shared budget belongs to the entire ApiKeyRateLimiter instance.

When a service is charged, its configured cost is deducted from the shared budget and, when PerCall limiting is enabled, the requested number of calls is deducted from that service's inventory.

The shared budget is explicitly shared across all registered services.

Example

var limiter = new ApiKeyRateLimiter
{
    SharedBudget = 100m
};

limiter.AddService(
    "WeatherAPI",
    CostPerCall: 2m,
    CurrentInventory: 10,
    MaxInventory: 10,
    LimitKind: ButlerApiLimitType.PerCall | ButlerApiLimitType.SharedBudget);

This configuration means:

  • The service has 10 available calls.
  • Each call costs 2 budget units.
  • Calls consume both inventory and shared budget.
  • The service cannot consume more inventory than it has.
  • The service cannot consume more shared budget than remains available.

For example, charging five calls consumes:

WeatherAPI inventory: 10 → 5
Shared budget:        100 → 90

Limit Types

ButlerApiLimitType determines which restrictions apply.

none

No inventory or shared-budget restriction is applied.

A service using none is effectively unlimited by this limiter.

PerCall

Each call consumes one unit of the service's inventory.

For example:

Inventory: 10
Charge:     3 calls

Inventory after charge: 7

SharedBudget

Each call consumes the configured CostPerCall from the limiter's shared budget.

For example:

Shared budget: 100
Cost per call:   2
Charge:          3 calls

Budget consumed: 6
Remaining:       94

Combining limits

PerCall and SharedBudget can be combined.

ButlerApiLimitType.PerCall | ButlerApiLimitType.SharedBudget

When both are enabled, a charge must satisfy both restrictions before anything is deducted.

This makes it possible to express policies such as:

"This API may be called no more than 100 times, and those calls may consume no more than 500 budget units."

Registering Services

Use AddService() to register a service.

limiter.AddService(
    "MyService",
    CostPerCall: 5m,
    CurrentInventory: 100,
    MaxInventory: 100,
    LimitKind: ButlerApiLimitType.PerCall |
               ButlerApiLimitType.SharedBudget);

By default, adding a service that already exists throws an InvalidOperationException.

If replacement is intentional, use ReplaceIfExists:

limiter.AddService(
    "MyService",
    CostPerCall: 10m,
    CurrentInventory: 50,
    MaxInventory: 50,
    LimitKind: ButlerApiLimitType.PerCall,
    ReplaceIfExists: true);

Negative costs are not supported. A zero cost should be used when a service should not consume shared budget.

Checking Permission Without Charging

Use CheckForCallPermission() when you want to determine whether a call is currently affordable without consuming inventory or budget.

if (limiter.CheckForCallPermission("MyService"))
{
    // The call can currently be afforded.
}

You can also check multiple calls:

if (limiter.CheckForCallPermission("MyService", 5))
{
    // Five calls can currently be afforded.
}

This method does not consume the allowance.

If the service does not exist, ServiceNonExistentException is thrown.

Checking and Charging

For the normal execution path, prefer:

CheckForCallPermissionAndCharge()

This performs the affordability check and, if permitted, consumes the appropriate budget and inventory. ButlerSDK uses this path in normal execution.

if (limiter.CheckForCallPermissionAndCharge("MyService"))
{
    // Execute the service call.
}
else
{
    // The service is currently over its available allowance.
}

For multiple calls:

if (limiter.CheckForCallPermissionAndCharge("MyService", 3))
{
    // Execute three calls.
}

If the call cannot be afforded because of the configured budget or inventory, the method returns false and does not consume the allowance.

The class performs the permission check and charge while holding its synchronization lock, preventing another thread from consuming the same allowance between those operations.

Charging Directly

ChargeService() can be used when the caller wants to perform the charge directly.

limiter.ChargeService("MyService", 1);

Unlike CheckForCallPermissionAndCharge(), this method communicates failure through exceptions rather than returning false.

For example, an insufficient budget or inventory results in OverBudgetException.

try
{
    limiter.ChargeService("MyService", 1);
}
catch (ApiKeyRateLimiter.OverBudgetException)
{
    // The requested charge cannot be satisfied.
}

Inspecting Services

You can determine whether a service has been registered:

if (limiter.DoesServiceExist("MyService"))
{
    // Service exists.
}

The total number of registered services is available through:

int count = limiter.ServiceCount;

Reading Inventory and Cost

Current service inventory:

decimal inventory =
    limiter.GetServiceInventory("MyService");

Configured maximum/reset inventory:

decimal limit =
    limiter.GetServiceLimit("MyService");

Current cost per call:

decimal cost =
    limiter.GetCurrentCost("MyService");

If the requested service does not exist, these operations throw ServiceNonExistentException.

Updating a Service

The limiter allows an application's policy to be changed at runtime.

Change the service's maximum/reset inventory:

limiter.AssignNewServiceLimit("MyService", 100);

Reset the current inventory back to that limit:

limiter.ResetServiceLimit("MyService");

Change the cost per call:

limiter.AssignNewCost("MyService", 2.5m);

Costs must be zero or greater.

Removing a Service

limiter.RemoveService("MyService");

Removing an unknown service is normally ignored.

If the caller wants removal of an unknown service to be treated as an error:

limiter.RemoveService(
    "MyService",
    PanicIfNonExistent: true);

This throws ServiceNonExistentException when the service is not registered.

Exceptions

ApiKeyRateLimiter exposes three primary exception types.

ServiceNonExistentException

Thrown when an operation requires a service that has not been registered.

Examples include:

CheckForCallPermission("UnknownService");
GetServiceInventory("UnknownService");
AssignNewCost("UnknownService", 1);

InventoryOrSeviceCostException

Thrown when an invalid inventory or cost operation is attempted.

Examples include negative costs and invalid inventory charges.

A zero-call charge is also restricted when PerCall inventory limiting is enabled.

OverBudgetException

Thrown when a requested charge cannot be satisfied by the available shared budget or service inventory.

Thread Safety

The limiter synchronizes access to its service collection, shared budget, and service inventory using an internal synchronization object.

Operations that inspect or modify limiter state acquire this lock before accessing the underlying state.

This is particularly important for:

CheckForCallPermissionAndCharge()

because the permission check and resulting charge occur as one synchronized operation rather than as two independently exposed operations.

A Complete Example

var limiter = new ApiKeyRateLimiter
{
    SharedBudget = 50m
};

limiter.AddService(
    "ExpensiveAPI",
    CostPerCall: 5m,
    CurrentInventory: 5,
    MaxInventory: 5,
    LimitKind: ButlerApiLimitType.PerCall |
               ButlerApiLimitType.SharedBudget);

if (limiter.CheckForCallPermissionAndCharge("ExpensiveAPI"))
{
    // Execute the API call.
}
else
{
    Console.WriteLine(
        "ExpensiveAPI is currently unavailable.");
}

After ten attempted calls, only the first five can succeed because the service has five units of inventory.

The shared budget provides a second boundary: even if inventory remains available, a call is rejected when its cost would exceed the remaining shared budget.

Design Intent

ApiKeyRateLimiter is deliberately a policy/enforcement component rather than a service execution component.

It does not make API calls, manage API keys, or determine how a service should actually be invoked.

Instead, it answers a narrower question:

"Is this operation allowed to consume this much of the application's configured allowance?"

That makes it useful as a boundary around ButlerSDK tools, API providers, metered services, or other operations where the application needs deterministic usage accounting.

The limiter itself does not decide what a service call means; the host application defines the services, costs, inventories, and applicable limits.

📄 License

Apache 2.0 License. See LICENSE for details.

Copyright 2025-2026 by Thomas Paul Betterly

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on ButlerSDK.ApiRateLimiter:

Package Downloads
ButlerSDK.Tools.Bench

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.2.5-preview 115 9/1/2026
1.2.0-preview 135 5/12/2026
1.1.0-preview 115 3/9/2026
1.0.0-preview 99 2/22/2026