Hold your horses.
Enforce a budget on any cloud resource. One atomic operation that answers yes or no. No setup, no migration, no suprise bills.
A recursive function with a missing break statement can do 116 billion database reads before lunch. Someone needs to hold their horses.
// 50,000 text messages a month, and not one more.
const sms = hh.group("sms")
.budget("messages", { limit: 50_000, renewal: renewal.monthly() });
// Sending to 200 people. Can we afford it?
const result = await sms
.draw("messages", 200)
.charge(); Only 120 left, so the answer is no. Nothing was spent, and the budget is untouched. Retry in 12d.
Declare your budgets
Limits travel inline with every draw, so there is no setup call and no migration. Change a constant, deploy, done.
import { HorseHolderClient, renewal } from "@horse-holder/client";
const hh = new HorseHolderClient({
apiKey: process.env.HORSEHOLDER_API_KEY,
});
const storage = hh.group("r2-storage")
.budget("put-ops", {
limit: 1_000_000,
warnings: [0.8],
renewal: renewal.monthly({ timezone: "America/Chicago" }),
})
.budget("egress-bytes", {
limit: 50_000_000_000,
renewal: renewal.monthly(),
}); Ask before you spend
Budgets in a group are drawn together or not at all. A refusal is a returned value, not a thrown error.
const result = await storage
.draw("put-ops", 1)
.draw("egress-bytes", object.size)
.idempotent(uploadId)
.charge();
if (!result.ok) {
return tooExpensive({ retryAfter: result.retryAfter });
}
await bucket.put(key, object); Or settle up afterwards
Hold an estimate, then commit what it actually cost. Crash instead, and the hold expires on its own.
const email = hh.group("transactional-email")
.budget("sends", { limit: 200_000, renewal: renewal.monthly() });
// Hold the whole recipient list, then correct it to what actually went out.
const lease = await email
.draw("sends", recipients.length)
.idempotent(campaignId)
.reserve({ ttlSeconds: 60 });
if (lease.ok) {
const sent = await deliverCampaign(recipients);
await lease.commit({ "sends": sent.length });
} Install
The client is on npm as @horse-holder/client. Zero dependencies, and baseUrl defaults here, so an API key
is all it needs.
npm install @horse-holder/client