# warehouse-time budgets (a data-source query is budgeted before it leaves)

> Every query an application sends to a registered data source is metered and budgeted, in milliseconds of provider time, per person, per application and per organisation. A query that would exceed the day's budget is refused before it is sent, so an unbounded read cannot run up a warehouse bill. Every application has a budget from the moment it exists: leaving the limit blank inherits the platform default, which is a cap and never an opening.

<!-- id: config-source-budget · area: config · stability: stable · html: https://osysharp.com/reference/config/source-budget/ -->

## Summary        {#summary}
A [registered data source](https://osysharp.com/reference/config/data-sources/) is somebody else's database, and somebody else's database bills
by the second. So the platform treats a warehouse query exactly as it treats a model call: it is **metered, and it
is refused before it leaves** when the day's budget is spent. Model calls have their own budgets, kept by
`Osysharp.Llm` ([Model spend — budgets, the meter and the price book (Llm kit)](https://osysharp.com/reference/agent/llm-spend/)) — a different unit, and a different ledger.

The budget is **milliseconds of provider time**: the wall time the platform spent inside your `IEntitySource` while
it answered. That is a proxy for warehouse-seconds and not the warehouse's own bill, because `IEntitySource` reports
neither cost nor elapsed — it is the honest unit the seam admits.

**An application always has a budget.** A blank limit is not "unlimited": it inherits the platform default. That is
the whole guarantee this exists for — nothing bills a warehouse with no cap in front of it, including an app nobody
has configured.

## Signature      {#signature}
```text
admin → org → Budgets
   Daily data-source time (ms)   ______   (blank = platform default · 0 = blocked)
```

## Description    {#description}

### Who is checked, and in what order  {#scopes}
Three scopes, any of which can refuse. The person is checked first, then the application, then its organisation.

| Scope | Set where | Blank means |
|---|---|---|
| **the person** | a `UserSourceBudget` row in your app, else the app's per-user default | the platform default — a cap, never unlimited |
| **the application** | admin → org → Budgets, per app | the platform default — a cap, never unlimited |
| **the organisation** | the same page, per org | the organisation caps nothing; only its applications' caps bind |

They are not alternatives. An organisation cap bounds what all of its applications may spend between them, so a
single app can be inside its own budget and still be refused by its organisation's — and one person can be refused
while the application still has most of its day left.

**The order is the point.** Someone over their own cap costs the application nothing: their query is refused before
the application's counter is touched. And a person's reservation is released when a later scope refuses, so a day
spent by the application does not also consume every person's day on calls that never left.

### What a refused query does  {#refusal}
It fails. It does not return an empty result, and it does not return stale rows — either of those would be a wrong
answer presented as a right one. The message names the scope, the limit and the usage:

```text title="what the caller sees when the day's budget is spent"
The data-source time budget for this application is exhausted (18,000,000 ms of 18,000,000 used).
It resets at 00:00 UTC.
```

The day starts at midnight in the **organisation's own time zone** when it declares one, not the server's — the same
declaration the LLM budget reads, so an application's warehouse day and its token day cannot disagree.

### The lint rule that gets ahead of it  {#lint}
`osy lint`'s **`cost-unbounded-read`** catches *a query materialized with no `Take`* — the shape that spends a
budget fastest, because the cost of a read with no bound is a fact about somebody else's table rather than about
your code. It is a SHOULD, not a refusal: an unbounded read is sometimes exactly right (a small reference table you
control), and where it is, say so at the site with
`[SuppressWarning("cost-unbounded-read")]` and a reason — see [[SuppressWarning]](https://osysharp.com/reference/diagnostics/suppress-warning/). The budget on this
page is the backstop for the ones nobody linted; the rule is how you stop paying for them in the first place.

### Why is it counted per source? {#per-source}
Usage is keyed by the **name you gave the source** in `app.DataSources`, not by the entity or the query. An app that
reads from two warehouses gets one usage row per warehouse per day, so a bill that surprises you can be attributed
to the warehouse that caused it.

```osy title="two sources, metered apart" test app=source-budget
using Osysharp.Sources;

class Ledger : IEntitySource {
  public const QueryFeatures Pushdown = QueryFeatures.All;
  public const string SecurityKitVersion = "1";

  public stream<SourceRow> Query(QueryPlan plan) {
    if (plan.Op == "Count") { yield return new SourceRow { Values = ["3"] }; }
  }

  public SourceSchema DescribeSchema() { return new SourceSchema(); }
}

// The NAME each entry is given is the name its spend is recorded under — "Books" and "Analytics",
// never the entity and never the query. An unexpected bill names the warehouse that caused it.
app.DataSources = [ new Ledger("Books"), new Ledger("Analytics") ];

[DataSource(DataSource.Books), ExternalName("SALES.ORDERS")]
entity Order {
  [Key, ExternalName("ORDER_ID")] string Number;
  security { allow read when IsAuthenticated; }
}

// Nothing here opts in. The budget is already in front of this call, because an app has one from
// the moment it exists — a blank limit inherits the platform default rather than meaning unlimited.
int OrderCount() { return Order.Count(); }
```

### How much may one person spend? {#per-user}
The per-person cap comes from `Osysharp.SourceBudget` — a capability your app opts into in order to read and
override it. **Both lines are needed**: the `use` in `app.osy` is what puts its tables in your database, and the
`using` is what lets this file name `UserSourceBudget`. Without the `use` there is no per-person cap in this app
at all, and the application and organisation caps are what bound a query.

```osy title="a per-person warehouse cap, and an app whose admins set it" test app=source-budget-per-user
using Osysharp.SourceBudget;     // and `use Osysharp.SourceBudget;` in app.osy

[Role] enum AppRole { Admin }

[Principal] entity User {
  [Unique, MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

[RoleGrant] entity RoleGrant {
  [Required] User Grantee;
  [Required] AppRole Level;
  security { allow read when IsAuthenticated; }
}

policy IsAdmin => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Admin);

// The cap for one person, in milliseconds per budget day. 0 blocks them; no row at all means the
// application's per-user default applies, and if that is blank too, the platform's.
partial entity UserSourceBudget {
  security {
    // The capability grants `read` to the person it names and nothing else — nobody raises their own cap.
    // An app whose admins set caps grants the write here, on its own terms.
    allow create, update when IsAdmin;
  }
}

// …and this is what the grant is FOR. Without the partial above, there is no way to write this table
// from Osy# at all.
// ⚠ NO EXECUTION-SIDE ATTRIBUTE HERE, DELIBERATELY. A function that writes an entity is server-side by
// INFERENCE — the platform decides the side, and the reference must never teach the override. Guarded by
// `ExecutionSideIsNotAdvertisedTests.NoPageInTheReference_NamesTheOverride`, which scans the BODY of every
// embedded page, so even naming the attribute in a comment like this one fails it. One was added here on
// 2026-09-26 while making this fence compiled, and the gate caught it.
void SetCap(Guid who, long millis) {
  var cap = new UserSourceBudget { UserId = who, DailySourceMillisLimit = millis };
}
```

⚑ **This example was `syntax` — illustrative, deliberately not compiled — until 2026-09-26, and it described a
program the compiler refused.** `UserSourceBudget` carried the default `[Authority(Narrowable)]`, so a partial
could only ever TIGHTEN what the capability granted, and the capability granted `read`: the `allow create, update`
above was rejected by name. The entity is published `[Authority(Extensible)]` now — a per-person cap is the app
owner's choice, and the capability ships the table precisely so an app can set one — and this fence is COMPILED, so it cannot describe a refused
program again without going red.

Three values are consulted, in order, and the first that exists wins:

1. a `UserSourceBudget` row for that person, in your app's own data;
2. the application's per-user default, set beside its own cap in admin → org → Budgets;
3. the platform default.

**Background work has no person**, so there is no per-person counter to charge: a workflow step, a scheduled job or
a system task is bounded by the application and organisation caps alone. That is a gap in *attribution*, never in
enforcement — no query escapes a cap by having nobody behind it.

⚠ This capability bounds data sources and nothing else. What a model call may spend is `Osysharp.Llm`'s, declared
with that kit: a budget filed under the wrong noun makes the manifest — the one place "what may this app reach" is
answerable — wrong for everyone reading it afterwards.

### Does it hold across regions? {#across-zones}
Yes. On a platform running your application in more than one hosting zone, each zone enforces the organisation's cap
against its own spend **plus what the other zones have already reported** — so the budget is one budget, not one per
zone. Near the ceiling the control plane rations what remains between the zones still running. A cap is therefore
enforced eventually rather than instantaneously: the overshoot between two syncs is bounded by what remains, which
is small precisely when it matters.

The application's cap and its per-person default travel to each zone together, so a cap you raise takes effect on
the machines actually running the queries and not only on the page you set it on. A per-person cap is counted in
each zone on its own — one person's day is not pooled across regions.

## Examples       {#examples}

```text title="blocking a source outright while you investigate"
admin → org → Budgets → (the app) → Daily data-source time (ms): 0
```
`0` blocks; blank restores the platform default. A blocked app's queries are refused before they leave, and the
refusal is counted, so the usage page shows the attempts.

## Notes          {#notes}
- **The unit is provider time, not rows and not bytes.** A query that returns one row after a forty-second scan
  costs forty seconds, which is the number the warehouse charges for and the number you want bounded.
- **A refusal is recorded separately from an AI refusal.** A warehouse query refused for warehouse time never
  touched the token budget, so it never appears as an AI refusal — the two counts are carried apart precisely so
  the page explaining why the AI stopped answering does not report calls the AI never received.
- **A person's warehouse day and their AI day are separate budgets**, deliberately. They are counted in different
  units against different limits, and someone who has spent their model tokens can still read a warehouse.

## See also       {#see-also}
- [data held somewhere else (app.DataSources)](https://osysharp.com/reference/config/data-sources/) — registering the sources this budget bounds
- [Model spend — budgets, the meter and the price book (Llm kit)](https://osysharp.com/reference/agent/llm-spend/) — what model calls may spend: budgets, the meter and the price book of `Osysharp.Llm`
- [The Snowflake kit — warehouse tables as entities, and exports back](https://osysharp.com/reference/config/snowflake/) · [The Databricks kit — lakehouse tables as entities, and exports back](https://osysharp.com/reference/config/databricks/) — the two shipped providers
