Back to Blog
6 min read

Azure Cosmos DB Serverless - Pay Per Request Database

Azure Cosmos DB serverless is a consumption-based offering that charges you only for the request units consumed and storage used. This model is perfect for development, testing, and applications with intermittent traffic patterns.

Understanding Serverless vs Provisioned Throughput

AspectServerlessProvisioned Throughput
BillingPer request (RU/s consumed)Per hour (RU/s reserved)
Max RU/s5,000 RU/s burstUnlimited (manual scaling)
Best forIntermittent workloadsConsistent high traffic
ScalingAutomaticManual or autoscale
StorageUp to 50 GB per containerUnlimited

Creating a Serverless Account

# Create serverless Cosmos DB account
az cosmosdb create \
    --name mycosmosdb-serverless \
    --resource-group myResourceGroup \
    --locations regionName=eastus \
    --capabilities EnableServerless \
    --default-consistency-level Session

# Create database
az cosmosdb sql database create \
    --account-name mycosmosdb-serverless \
    --resource-group myResourceGroup \
    --name myDatabase

# Create container
az cosmosdb sql container create \
    --account-name mycosmosdb-serverless \
    --resource-group myResourceGroup \
    --database-name myDatabase \
    --name myContainer \
    --partition-key-path "/partitionKey"

.NET SDK Integration

// CosmosDbService.cs
using Microsoft.Azure.Cosmos;

public class CosmosDbService
{
    private readonly Container _container;

    public CosmosDbService(string connectionString, string databaseName, string containerName)
    {
        var cosmosClient = new CosmosClient(connectionString, new CosmosClientOptions
        {
            SerializerOptions = new CosmosSerializationOptions
            {
                PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
            },
            ConnectionMode = ConnectionMode.Direct,
            MaxRetryAttemptsOnRateLimitedRequests = 9,
            MaxRetryWaitTimeOnRateLimitedRequests = TimeSpan.FromSeconds(30)
        });

        _container = cosmosClient.GetContainer(databaseName, containerName);
    }

    public async Task<T> CreateItemAsync<T>(T item, string partitionKey)
    {
        var response = await _container.CreateItemAsync(
            item,
            new PartitionKey(partitionKey));

        Console.WriteLine($"Request charge: {response.RequestCharge} RU");
        return response.Resource;
    }

    public async Task<T?> GetItemAsync<T>(string id, string partitionKey)
    {
        try
        {
            var response = await _container.ReadItemAsync<T>(
                id,
                new PartitionKey(partitionKey));

            Console.WriteLine($"Request charge: {response.RequestCharge} RU");
            return response.Resource;
        }
        catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
            return default;
        }
    }

    public async Task<IEnumerable<T>> QueryItemsAsync<T>(string query, string? partitionKey = null)
    {
        var queryDefinition = new QueryDefinition(query);
        var queryOptions = new QueryRequestOptions();

        if (partitionKey != null)
        {
            queryOptions.PartitionKey = new PartitionKey(partitionKey);
        }

        var results = new List<T>();
        double totalRU = 0;

        using var iterator = _container.GetItemQueryIterator<T>(queryDefinition, requestOptions: queryOptions);

        while (iterator.HasMoreResults)
        {
            var response = await iterator.ReadNextAsync();
            totalRU += response.RequestCharge;
            results.AddRange(response);
        }

        Console.WriteLine($"Total request charge: {totalRU} RU");
        return results;
    }

    public async Task<T> UpsertItemAsync<T>(T item, string partitionKey)
    {
        var response = await _container.UpsertItemAsync(
            item,
            new PartitionKey(partitionKey));

        Console.WriteLine($"Request charge: {response.RequestCharge} RU");
        return response.Resource;
    }

    public async Task DeleteItemAsync(string id, string partitionKey)
    {
        var response = await _container.DeleteItemAsync<dynamic>(
            id,
            new PartitionKey(partitionKey));

        Console.WriteLine($"Request charge: {response.RequestCharge} RU");
    }
}

Optimizing for Serverless

1. Efficient Partition Key Design

// Good: Natural partition key with even distribution
public class Order
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string CustomerId { get; set; }  // Partition key
    public DateTime OrderDate { get; set; }
    public List<OrderItem> Items { get; set; } = new();
    public decimal Total { get; set; }
}

// Usage
var order = new Order
{
    CustomerId = "customer-123",
    OrderDate = DateTime.UtcNow,
    Items = new List<OrderItem> { /* items */ },
    Total = 99.99m
};

await cosmosService.CreateItemAsync(order, order.CustomerId);

2. Point Reads Over Queries

// Efficient: Point read (1 RU for 1KB document)
var order = await cosmosService.GetItemAsync<Order>("order-id", "customer-123");

// Less efficient: Query (higher RU cost)
var orders = await cosmosService.QueryItemsAsync<Order>(
    "SELECT * FROM c WHERE c.id = 'order-id'");

3. Batch Operations

public async Task<List<T>> CreateBatchAsync<T>(
    IEnumerable<T> items,
    Func<T, string> partitionKeySelector) where T : class
{
    var results = new List<T>();

    // Group items by partition key for transactional batch
    var groups = items.GroupBy(partitionKeySelector);

    foreach (var group in groups)
    {
        var batch = _container.CreateTransactionalBatch(new PartitionKey(group.Key));

        foreach (var item in group)
        {
            batch.CreateItem(item);
        }

        using var response = await batch.ExecuteAsync();

        if (response.IsSuccessStatusCode)
        {
            for (int i = 0; i < response.Count; i++)
            {
                results.Add(response.GetOperationResultAtIndex<T>(i).Resource);
            }
        }

        Console.WriteLine($"Batch request charge: {response.RequestCharge} RU");
    }

    return results;
}

4. Projection Queries

// Return only needed fields to reduce RU cost
public async Task<IEnumerable<OrderSummary>> GetOrderSummariesAsync(string customerId)
{
    var query = @"
        SELECT c.id, c.orderDate, c.total
        FROM c
        WHERE c.customerId = @customerId";

    var queryDefinition = new QueryDefinition(query)
        .WithParameter("@customerId", customerId);

    var results = new List<OrderSummary>();

    using var iterator = _container.GetItemQueryIterator<OrderSummary>(
        queryDefinition,
        requestOptions: new QueryRequestOptions
        {
            PartitionKey = new PartitionKey(customerId)
        });

    while (iterator.HasMoreResults)
    {
        var response = await iterator.ReadNextAsync();
        results.AddRange(response);
    }

    return results;
}

public record OrderSummary(string Id, DateTime OrderDate, decimal Total);

Azure Functions Integration

Perfect pairing for serverless architecture:

// OrderFunction.cs
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;

public class OrderFunction
{
    private readonly CosmosDbService _cosmosService;

    public OrderFunction(CosmosDbService cosmosService)
    {
        _cosmosService = cosmosService;
    }

    [Function("CreateOrder")]
    public async Task<HttpResponseData> CreateOrder(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
    {
        var order = await req.ReadFromJsonAsync<Order>();

        if (order == null)
        {
            var badRequest = req.CreateResponse(System.Net.HttpStatusCode.BadRequest);
            return badRequest;
        }

        order.Id = Guid.NewGuid().ToString();
        var created = await _cosmosService.CreateItemAsync(order, order.CustomerId);

        var response = req.CreateResponse(System.Net.HttpStatusCode.Created);
        await response.WriteAsJsonAsync(created);
        return response;
    }

    [Function("GetOrder")]
    public async Task<HttpResponseData> GetOrder(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "orders/{customerId}/{orderId}")]
        HttpRequestData req,
        string customerId,
        string orderId)
    {
        var order = await _cosmosService.GetItemAsync<Order>(orderId, customerId);

        if (order == null)
        {
            return req.CreateResponse(System.Net.HttpStatusCode.NotFound);
        }

        var response = req.CreateResponse(System.Net.HttpStatusCode.OK);
        await response.WriteAsJsonAsync(order);
        return response;
    }

    [Function("ProcessOrderChanges")]
    public void ProcessChanges(
        [CosmosDBTrigger(
            databaseName: "myDatabase",
            containerName: "orders",
            Connection = "CosmosDbConnection",
            LeaseContainerName = "leases",
            CreateLeaseContainerIfNotExists = true)]
        IReadOnlyList<Order> changes)
    {
        foreach (var order in changes)
        {
            Console.WriteLine($"Order changed: {order.Id}, Total: {order.Total}");
            // Process the change (send notifications, update analytics, etc.)
        }
    }
}

Cost Optimization Tips

  1. Monitor RU consumption: Use Azure Monitor to track usage patterns
  2. Optimize queries: Always use partition keys in queries
  3. Consider provisioned throughput: If consistent >$50/month, evaluate switching
  4. Use TTL: Automatically delete old data to save storage costs
// Enable TTL at container level
var containerProperties = new ContainerProperties
{
    Id = "myContainer",
    PartitionKeyPath = "/partitionKey",
    DefaultTimeToLive = -1  // Enables TTL, items need ttl property
};

// Set TTL on individual items
public class LogEntry
{
    public string Id { get; set; }
    public string PartitionKey { get; set; }
    public string Message { get; set; }

    [JsonPropertyName("ttl")]
    public int TimeToLive { get; set; } = 86400; // 24 hours
}

Conclusion

Cosmos DB serverless is excellent for:

  • Development and testing environments
  • Applications with unpredictable or bursty traffic
  • Microservices with low, intermittent throughput
  • Event-driven architectures with Azure Functions

The consumption model eliminates capacity planning and reduces costs for workloads that don’t need constant throughput. Combined with Azure Functions, it creates a truly serverless data tier.

Michael John Pena

Michael John Pena

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.