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.

sms.charge(…)
// 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();
402 budget_exceeded
messages 49,880 / 50,000

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