The Kladde stays on Azure SQL (K3, lesson 3.6.4). Next to it, Kladde.Verlauf is built: a service that is fed from the Kladde and uses Cosmos DB for NoSQL, because that is where the triggers from K3 are available. This lesson lays the foundation: Which resources exist, what do you set at which level, and how much throughput does which container get?

Prior knowledge. The five terms (partition key, request units, consistency levels, item limit, change feed) and the emulators are in "K3, lesson 3.6.4" and are not repeated; "K4, lesson 4.11.4" goes deeper into the consistency levels.

What was run. The C# programs of this lesson were compiled with Microsoft.Azure.Cosmos 3.64.0 (latest stable version on the day of writing) and Newtonsoft.Json 13.0.4; the calculation programs ran without an account, and the Bicep example was compiled with Bicep 0.43.8. Nothing was run against Azure; every place that needs an account says so. Numbers in tables are assumptions of the task, not measurements.

The resource model

An account contains databases, a database contains containers, a container contains items. Learn calls the database the unit of management and the container the unit of scale for throughput and storage. On the account you set regions, default consistency level, sign-in (key or Entra), network and backup; on the container you set the partition key (cannot be changed later), the throughput, the indexing policy, the time to live (TTL) and unique keys.

Limits according to Learn (page "service quotas and default limits"): at most 500 databases and containers together per account (cannot be raised), 1,000,000 RU/s per container (more on request), 10,000 RU/s and 20 GB per logical partition. For management calls an account has only 240 RU/s; that is why names belong in the configuration, and containers are not "checked" on every request (lesson 1.2).

The containers of Kladde.Verlauf are fixed as F-03: appointments, events, conversations, knowledge, leases. A container per office would be an anti-pattern that Learn does not recommend for multitenant applications; tenants are values of the partition key (lesson 1.4).

The throughput modes

provisioned (manual) autoscale serverless
You set a fixed number of RU/s maximum Tmax nothing
Smallest value 400 RU/s Tmax at least 1,000 –
The service uses always the number 0.1 × Tmax to Tmax, instantly per request
Billing per hour the number per hour the highest value reached, at least 0.1 × Tmax consumed RU and storage
Regions any number any number exactly one

According to Learn on the day of writing:

  • Autoscale costs 1.5 times the manual rate per RU/s in an account with one write region. That gives Learn's rule of thumb: If you use the maximum in less than about 66% of the hours, autoscale is cheaper; otherwise the fixed number is (1 ÷ 1.5 ≈ 0.67). The metric for this is "Normalized RU Consumption".
  • Serverless suits development, tests and load with long pauses (Learn: under 10% ratio of average to peak). A serverless container starts at 5,000 RU/s, and per physical partition it goes up to 5,000 RU/s; you can neither set nor read the throughput, and there is no shared database.
  • The free tier from K3 (1,000 RU/s, 25 GB) fits the test account.

Database or container? Throughput can sit on the database and is then shared by up to 25 containers, or on the container, where it belongs to that container alone. For most workloads Learn recommends throughput on the container, because it is predictable. A container cannot be switched from dedicated to shared throughput or the other way around; the way is a new container and copying the data.

The choice for F-05. The development account of Kladde.Verlauf is serverless. The production assumption is autoscale with a maximum per container. Reason: The load varies with the offices' working day (F-02) and with nightly syncs from the Kladde.

Measuring request units

Every response of the SDK carries its consumption in RequestCharge, including the FeedResponse of a query. According to Learn, reading an item of about 1 KB by id and partition key costs 1 RU. The consumption is determined by item size, number of properties and indexed paths, point read versus query, the structure of the query, and, with Strong and Bounded Staleness, double for reads. The same query on the same data costs the same every time.

Measuring means writing it down. This counter collects the cost per operation from every Response<T>:

C#
public void Record<T>(string operation, Response<T> response)
{
    _byOperation.TryGetValue(operation, out var current);
    _byOperation[operation] = (current.Count + 1, current.Total + response.RequestCharge);
}

For the test without an account, a stand-in inherits from ItemResponse<T> (the response object is meant for that) and sets the header from which the SDK reads RequestCharge: new Headers { ["x-ms-request-charge"] = "5.5" }. A run with two writes at 5.5 RU each and one read at 1 RU gave:

read appointment   1 call(s), 1 RU
write appointment  2 call(s), 11 RU

Against an account you see the same number in the portal (Data Explorer, run query, "Query Stats") and in the diagnostics object of every response (lesson 1.2).

Estimating the need up front

Learn offers the cost estimator (cosmos.azure.com/costestimator). Also calculate it yourself, so that the assumption is part of your design. Two rules from Learn's page on the limits, as code in Rules: The smallest maximum is MAX(1000, 10 × GB, earlier maximum ÷ 10), rounded up to 1,000; billing per hour is MAX(0.1 × Tmax, highest value of the hour).

With the examples from Learn (50,000 RU/s as the highest earlier value):

manual minimum, 0 GB:          400 RU/s
manual minimum, 2000 GB:       2000 RU/s
autoscale max minimum, 20 GB:  5000 RU/s
autoscale max minimum, 2000 GB:20000 RU/s
autoscale max 4000, busiest hour 3500 -> billed 3500 RU/s
autoscale max 4000, idle hour          -> billed 400 RU/s

These are the numbers of the Learn pages. Remember: Whoever has once provisioned a lot cannot go back arbitrarily far, and every gigabyte raises the lower bound (1 RU/s per GB with a fixed number, 10 RU/s per GB with autoscale).

The assumption of the task (F-05)

The maximum per container follows from a peak load that you write down. All values are assumptions: 1 RU per point read of an item of about 1 KB (Learn); the task estimates the other RU per operation until an account measures them, and the buffer is a factor of 3.

Container Assumed peak (per second) Peak RU/s Maximum
appointments 100 point reads at 1, 40 writes at 6, 10 month queries at 12 460 2,000
events 60 writes at 6, 5 day queries at 15 435 2,000
conversations 20 writes at 8, 20 point reads at 2 200 1,000
knowledge 10 writes at 30, 5 vector queries at 50 550 2,000
leases 10 updates at 6 60 1,000

The program calculated peak times 3, rounded up to full 1,000, at least 1,000, and printed 2,000, 2,000, 1,000, 2,000 and 1,000, each with the range from one tenth up to the maximum (for 2,000 that is 200 to 2,000).

These maximums apply unchanged for the whole course (F-05). Later lessons calculate with them. If they are not enough, the lesson changes the assumption explicitly.

Creating: in the SDK and in Bicep

In the SDK, CreateDatabaseIfNotExistsAsync and CreateContainerIfNotExistsAsync create the resources, once at setup (Learn: every call is a management call). The code needs an account and was only compiled, not run against Azure:

C#
using var client = new CosmosClient("https://<account>.documents.azure.com:443/", new DefaultAzureCredential());
Database database = await client.CreateDatabaseIfNotExistsAsync("kladde");

var properties = new ContainerProperties("appointments", ["/officeId", "/month"]);
Container appointments = await database.CreateContainerIfNotExistsAsync(
    properties, ThroughputProperties.CreateAutoscaleThroughput(autoscaleMaxThroughput: 2000));

The list of paths is the hierarchical key (lesson 1.4). ReadThroughputAsync also returns the lower bound that the service demands for the container.

In Bicep (K3), the setup belongs in one file. Excerpt: account without keys (F-01), default consistency Session (F-04) and the containers from F-03; the complete file was compiled with az bicep build, not deployed:

bicep
resource account 'Microsoft.DocumentDB/databaseAccounts@2025-04-15' = {
  name: accountName
  location: 'germanywestcentral'
  kind: 'GlobalDocumentDB'
  properties: {
    databaseAccountOfferType: 'Standard'
    locations: [{ locationName: 'germanywestcentral', failoverPriority: 0 }]
    consistencyPolicy: { defaultConsistencyLevel: 'Session' }
    disableLocalAuth: true
    capabilities: serverless ? [{ name: 'EnableServerless' }] : []
  }
}

Each container additionally gets partitionKey (hierarchical: kind: 'MultiHash', version: 2, according to Learn), defaultTtl and, except for serverless, autoscaleSettings with the maximum from the table.

disableLocalAuth: true switches the keys off (roles and Entra: lesson 3.1). The 90 days of expiry for events are 90 × 86,400 = 7,776,000 seconds. Open: Whether autoscale, serverless and hierarchical keys are available in germanywestcentral was not settled by the pages read; check Learn's region overview on the day of deployment.

The consistency level as a setting

The default level belongs to the account, here Session (F-04), and applies to all reads and queries. A client may only weaken it, with CosmosClientOptions.ConsistencyLevel or per request in ItemRequestOptions; whoever needs stronger guarantees for a single request has, according to Learn, the ReadConsistencyStrategy since .NET SDK 3.46. If the default level changes on the account, restart the application so that the SDK picks it up. Which level guarantees what is in "K4, lesson 4.11.4".

Exercise 1

Part A, without an account. Create ru.cs: Rules according to the formulas of this lesson (fixed number: MAX(400, 1 × GB, earlier maximum ÷ 100)), ChargeMeter and the assumptions of the five containers.

  1. Check the examples from Learn: The lower bound for a fixed number must be exactly 500 at 20 GB and an earlier maximum of 50,000, and exactly 2,000 at 2,000 GB.
  2. The nightly filling of knowledge writes 2,000 text sections at 30 RU. How long does it take with the maximum of 2,000 RU/s if nothing else is running, and what is billed for that hour?
  3. A new container audit is to sustain 20 writes per second at 6 RU. What maximum results?

Part B, with a test account (not run against Azure): Run create.cs in an account with provisioned throughput. A serverless account does not tolerate the throughput setting (Learn: error when creating and when reading the throughput); leave it out there and write down only the cost of one write. Cleaning up: await database.DeleteAsync() or delete the resource group of the test account.

How to check it: Part A ends with exit code 0 and the numbers of the solution.

Solution 1

  1. Rules.MinManual(20, 50_000) is MAX(400, 20, 500) = 500; at 2,000 GB it is 2,000 (the run above shows it; for autoscale 5,000 and 20,000).
  2. 2,000 × 30 = 60,000 RU, divided by 2,000 RU/s gives 30 seconds, if nothing else is running. The hour is billed at 2,000 RU/s (BilledAutoscale(2000, 2000)), ten times as much as a quiet hour at 200. Whoever wants to avoid that spreads the intake out; the SDK retries 429 by itself (lesson 1.2).
  3. 20 × 6 = 120 RU/s, times 3 is 360, but at least 1,000: so 1,000, equal to the lower bound of an empty container (it rises by 10 RU/s per GB).

Part B stays unchecked here. The measured number replaces your assumption of 6 RU in the table.

What you can do now

  • You choose between provisioned throughput, autoscale and serverless and justify it with load, regions and the rule of about 66%.
  • You measure consumption with RequestCharge and calculate the maximum per container from a recorded assumption.
  • You create a database and container in the SDK and in Bicep and switch the keys off on the account.
  • You know that the client may only weaken the consistency level.

Further reading