← FinOpsAid blog

GitHub budgets can now expire. That quietly removes a hard stop.
GitHub added optional expiration dates to individual user budgets on September 1, 2026. The convenience is real — temporary overrides used to need manual cleanup. But user-level budgets are the only budgets that always block spending, and an expiry hands the user to a higher-level budget that only alerts by default. Set the date knowing which of those two you're choosing.
An expiration date on a budget sounds like housekeeping. For every other budget scope it would be. For user budgets it isn't, because user budgets are the only ones GitHub blocks with unconditionally — and an expiry is therefore a scheduled change in what happens when someone hits their limit.
Worth five minutes before you set one, because the feature is genuinely useful and the failure mode is silent.
What GitHub shipped
On September 1, 2026, GitHub added optional expiration dates to individual user budgets for Copilot Business and Copilot Enterprise (GitHub Changelog, September 2026). GitHub removes the budget automatically once the date passes, and the user falls back to whichever higher-level budget applies — a cost center budget, or the universal one.
Before this, a temporary raise had to be un-raised by hand. At any real headcount that's a recurring task nobody owns, so overrides accumulated and became permanent by default. The fix is the right one.
Three options are available when creating or editing a budget:
| Option |
Behaviour |
| No expiration |
The default. Unchanged from previous behaviour. |
| Expires at the start of the next billing cycle |
Reverts when the meter resets. |
| Expiration on a specific date |
A date you choose, YYYY-MM-DD. |
You can change or clear the date at any time. In the REST API it's the expires_at field, which accepts future dates only, and takes null to remove an existing expiry (GitHub Docs, Budgets REST API, retrieved 2026-09-09).
The part that isn't housekeeping
GitHub's own documentation is unusually direct about this. On the option that stops spending when a limit is reached, it says: "This option is not available for user-level budgets, which always enforce a hard stop." And on how that interacts with everything above it: "A user-level budget can block a user even when the enterprise or cost center budget still has capacity" (GitHub Docs, Setting up budgets, retrieved 2026-09-09).
Read those two sentences together and the shape of the change is clear. A user budget is a wall. It blocks regardless of what the pool has left, and you can't turn the blocking off. Every other scope defaults to alerts — you have to select Stop usage when budget limit is reached to get a wall, and that option only exists for metered products and Advanced Security SKU budgets.
So when a user budget expires, the wall is gone. What's left is whatever the fallback budget does, and unless somebody enabled blocking on it, what it does is send email at 75%, 90% and 100%.
Scope names are the literal budget_scope values from GitHub's Budgets REST API. Enforcement behaviour and the fallback order are from GitHub's Setting up budgets documentation, retrieved 2026-09-09.
Only user budgets can expire
The API makes the limit explicit: expires_at is a user-scoped field. Budgets support seven scopes — enterprise, organization, repository, cost_center, multi_user_customer, multi_user_cost_center and user — and only the last one takes a date.
That rules out the thing a lot of finance teams will want first. You can't say "this cost center's ceiling goes back down on November 1." A temporary raise at cost-center level is still a manual reversal, and still a task somebody has to remember. If your quarter-end pattern is to lift a team's ceiling and drop it again, this release doesn't help you yet.
It also means the expiry mechanism can only ever release individuals back into the pool. Which is fine — that's where most temporary overrides live — but it's worth knowing before you plan a workflow around it.
Our finding: we model budgets in FinOpsAid too, and ours deliberately cannot do what GitHub's user budget does. Our budgets carry a unit — either billed or modeled dollars, or a raw AI-credit quantity — and they are read-side alerting only. There's a good reason, and it's not that blocking was hard to build: we're a read-only GitHub App, and the data underneath us is nightly-synced, with GitHub's own Copilot and billing metrics lagging roughly two to three days. A cap enforced against a number that's three days old isn't a cap. It's a late notification wearing a cap's clothing. We also don't model an expiry date, so a temporary raise in our tool is a manual reversal — exactly the chore GitHub just removed for user budgets, which is a fair reminder of where a native control beats a dashboard.
That's the general rule this release reinforces. A budget enforced at the source can block. Anything downstream of a sync can only tell you it already happened.
What to check before you set an expiry date
- Find out whether the fallback exists. If the user has no cost center budget and no universal budget, expiry doesn't hand them to a weaker control. It hands them to none. Query
GET /enterprises/{enterprise}/settings/billing/budgets and look for a multi_user_cost_center or multi_user_customer row before you rely on one.
- Check
prevent_further_usage on that fallback. If it's false, the wall becomes an email. If that's the outcome you want, fine — just choose it rather than inherit it.
- Look at the fallback's remaining headroom, not the user's. Each budget object carries its own
consumed_amount against its own budget_amount. What matters after the expiry is what the pooled budget has left this cycle, which is a different number from whatever the user's own budget had left.
- Prefer "start of the next billing cycle" for genuinely temporary raises. It lines the reversion up with the meter reset, so you're not handing someone a fresh unblocked window part-way through a cycle they've already been consuming.
- Set alert recipients on the fallback.
budget_alerting carries will_alert and alert_recipients. After an expiry, that list is the entire remaining control surface, so it should name someone who acts rather than a shared inbox.
- Don't use expiry as a way to avoid a conversation. An override that expires silently is a decision made by a calendar. If the raise was justified, make it permanent and adjust the pool; if it wasn't, say so.
The one-line version: an expiration date is a good feature for reverting a temporary raise, and a bad way to discover that nothing was catching the user underneath.
Where FinOpsAid fits
FinOpsAid reads your GitHub org read-only and shows credit consumption per user and per team against your allowance, alongside the rest of the bill — Actions, storage, Codespaces, runners. Our budgets page is where you'd set a threshold on that, and how to read the Copilot page covers the per-user credit view you'd use to decide whether an override was ever warranted. Two honest limits, both stated above: it alerts rather than blocks, and the underlying GitHub metrics lag two to three days. For an actual hard stop, use GitHub's own per-user budget — that's the point of this release. Related reading: cost centers and AI credit pools on how the pooling works, and budgets that catch overspend early on choosing thresholds worth acting on.
Frequently asked questions
Does a GitHub budget actually stop spending, or only warn?
It depends on the scope. User-level budgets always enforce a hard stop and the blocking cannot be disabled. Every other scope alerts by default at 75%, 90% and 100%, and blocks only if an administrator selects "Stop usage when budget limit is reached" — an option available for metered products and Advanced Security SKU budgets.
Can I set an expiration date on a cost center or organization budget?
No. In GitHub's Budgets REST API, expires_at is a user-scoped field only. Budgets support seven scopes including cost_center, organization and multi_user_customer, but only a user budget accepts an expiration date. Temporary raises at any other scope still have to be reversed manually.
What happens to a user the moment their budget expires?
GitHub removes the expired budget and the user falls back to the next applicable budget — their cost center's universal budget, or the enterprise-wide one. Because those alert rather than block by default, the user's unconditional hard stop is replaced by whatever enforcement that fallback was configured with, which may be none.
Sources
If you want to see per-user credit consumption against your allowance before you decide whose budget needs a ceiling, connect your GitHub org — read-only and free during beta — or explore the demo dashboard first.