Cosmos DB DeleteItemAsync 404: Partition Key Fix
Fix Azure Cosmos DB DeleteItemAsync returning 404 Resource Not Found despite successful queries. Learn partition key mismatches, query projections, C# SDK fixes, and verification steps for iothub container deletes by date range.
Why does Cosmos DB DeleteItemAsync return ‘Resource Not Found (404)’ for documents successfully queried by ID and partition key?
I’m using the following C# code to delete documents from a Cosmos DB container (iothub in database telemetrydata) based on a date range in c.Body.TimeStamp:
public async Task<int> DeleteDocumentsByDateRangeSimple(DateTime fromDate, DateTime toDate)
{
int deletedCount = 0;
try
{
using CosmosClient client = new CosmosClient($"AccountEndpoint={endpoint};AccountKey={accountKey}");
Microsoft.Azure.Cosmos.Database db = client.GetDatabase("telemetrydata");
Container container = db.GetContainer("iothub");
string sfromDate = fromDate.ToString("yyyy-MM-dd\\THH:mm:ss");
string stoDate = toDate.ToString("yyyy-MM-dd\\THH:mm:ss");
var query = new QueryDefinition(
"SELECT c.id, c.partitionKey FROM c WHERE c.Body.TimeStamp >= @minDate AND c.Body.TimeStamp <= @maxDate"
)
.WithParameter("@minDate", sfromDate)
.WithParameter("@maxDate", stoDate);
using FeedIterator<dynamic> feed = container.GetItemQueryIterator<dynamic>(query);
while (feed.HasMoreResults)
{
var response = await feed.ReadNextAsync();
foreach (var doc in response)
{
try
{
string id = doc.id?.ToString()?.Trim('"');
string partitionKey = doc.partitionKey?.ToString()?.Trim('"');
if (string.IsNullOrEmpty(id))
{
Debug.WriteLine($"Skipping document with missing id");
continue;
}
var deleteResponse = await container.DeleteItemAsync<dynamic>(id, new PartitionKey(partitionKey));
if (deleteResponse.StatusCode == System.Net.HttpStatusCode.NoContent)
{
deletedCount++;
}
else
{
Debug.WriteLine($"Delete returned status {deleteResponse.StatusCode} for id={id}, pk={partitionKey}");
}
}
catch (Exception ex)
{
Debug.WriteLine($"Failed to delete document id={doc.id}, partitionKey={doc.partitionKey}: {ex.Message}");
}
}
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"DeleteDocumentsByDateRangeSimple() : Unexpected error : {ex.Message}");
}
return deletedCount;
}
The query returns documents, but every delete fails with:
Failed to delete document id=6aa773ff-54cb-4837-9620-653d58a2bf5f, partitionKey=VK_THC_GreenRoom: Response status code does not indicate success: NotFound (404); Substatus: 0; ActivityId: a6858546-110a-45e6-8fec-e56ef7571b44; Reason: (Errors : ["Resource Not Found."]);
Manual search in the Azure portal confirms the document exists with the exact ID and partition key. What am I missing that’s causing the ‘Resource Not Found’ error during deletion?
Cosmos DB DeleteItemAsync throws a 404 “Resource Not Found” error when the partition key value fed into the delete operation doesn’t precisely match what’s stored at the container’s defined partition key path—like /Body/partitionKey in your Azure Cosmos DB iothub setup—despite queries pulling documents just fine. Queries fan out across partitions, but deletes are point operations that route directly, so even a tiny mismatch (quotes, whitespace, or wrong nesting) bombs out. Switch your query to project c.Body.partitionKey instead of c.partitionKey, trim strings carefully, and test with ReadItemAsync first to nail it.
Contents
- Why Cosmos DB DeleteItemAsync Returns 404 Despite Successful Queries
- Common Cosmos DB Partition Key Mismatches in Azure Database Containers
- Fixing Query Projections and String Extraction in C# SDK
- SDK Behaviors and Potential Bugs Causing False 404s
- Step-by-Step Verification in Azure Portal and Code
- Optimized Code Solution and Bulk Delete Best Practices
- Sources
- Conclusion
Why Cosmos DB DeleteItemAsync Returns 404 Despite Successful Queries
You’ve nailed the query—docs pop up with the right IDs and what looks like the partition key. But then DeleteItemAsync hits you with that infuriating 404: “Resource Not Found,” complete with Substatus 0 and an ActivityId you could chase down in diagnostics. Why?
Queries in Azure Cosmos DB are forgiving. They scatter across all partitions in your iothub container, matching on c.Body.TimeStamp without needing the exact partition key upfront. Deletes? Totally different beast. They’re point reads that route straight to one partition using the ID and PartitionKey you provide. Miss by a hair—wrong path, extra quotes from JSON deserialization, case sensitivity—and Cosmos DB says, “Never heard of it.”
In your case, the error screams routing failure: id=6aa773ff-54cb-4837-9620-653d58a2bf5f, partitionKey=VK_THC_GreenRoom. Portal shows it exists. Queries grab it. But delete can’t find the partition. This isn’t rare in IoT Hub telemetry containers, where partition keys often nest deep in Body objects from device twins or messages.
Think about your date filter too. yyyy-MM-dd\\THH:mm:ss with the escaped T? Queries tolerate loose ISO-ish strings, but if c.Body.TimeStamp has microseconds or Zulu offsets, edge cases slip through. Still, the smoking gun is that partition key projection.
Common Cosmos DB Partition Key Mismatches in Azure Database Containers
Partition keys aren’t just values—they’re paths defined at container creation, like /partitionKey or /Body/partitionKey for skewed IoT data. Your query grabs c.partitionKey, assuming it’s top-level. But in telemetrydata.iothub, it’s probably nested. Hello, mismatch.
Users hit this constantly. One dev queried top-level c.partitionKey for a container partitioned on /Body/partitionKey, got perfect query results, but deletes 404’d every time. Exact parallel to yours. The value “VK_THC_GreenRoom” looks good post-trim, but if the doc stores it at doc.Body.partitionKey, your dynamic projection pulls null or wrong field.
Other gotchas:
- JSON strings:
dynamicin C# SDK deserializes as JValue, sodoc.partitionKey.ToString()wraps in quotes. YourTrim('"')helps, but misses escapes or whitespace. - Case sensitivity: “Greenroom” vs. “GreenRoom”? Cosmos DB doesn’t forgive.
- Orphans: Docs without partition key values (null/empty) query fine but delete with 404, per Microsoft’s troubleshooting guide.
- Post-query changes: Rare, but if concurrency edits the doc between query and delete, poof.
In Azure databases like yours, IoT workloads amplify this—millions of skewed partitions mean exact routing is non-negotiable. Wrong path? RU waste and 404 spam.
Fixing Query Projections and String Extraction in C# SDK
Rip out that query. Change to SELECT c.id, c.Body.partitionKey AS partitionKey FROM c WHERE c.Body.TimeStamp >= @minDate AND c.Body.TimeStamp <= @maxDate. Boom—pulls from the right nest. Then handle dynamic properly with Newtonsoft.Json for robust parsing.
Your code’s close, but doc.partitionKey?.ToString()?.Trim('"') fights JSON quirks. Better: Cast to JObject, navigate safely. Here’s the tweak:
var query = new QueryDefinition(
"SELECT c.id, c.Body.partitionKey AS partitionKey FROM c WHERE c.Body.TimeStamp >= @minDate AND c.Body.TimeStamp <= @maxDate"
)
.WithParameter("@minDate", fromDate.ToString("yyyy-MM-ddTHH:mm:ssZ")) // Drop \\T, add Z for ISO
.WithParameter("@maxDate", toDate.ToString("yyyy-MM-ddTHH:mm:ssZ"));
In the loop:
JObject docObj = JObject.FromObject(doc); // Safe JSON handling
string id = (string)docObj["id"]?.ToString()?.Trim();
string partitionKey = (string)docObj["partitionKey"]?.ToString()?.Trim(); // Now from Body!
Why JObject? dynamic can mangle nested strings. This extracts cleanly, no quote fights. Test it—queries should now yield the exact PK value stored at /Body/partitionKey.
Date strings? Ditch the backslash escape; Cosmos DB parses standard ISO 8601. If TimeStamp is epoch or variant, adjust the WHERE clause.
One more: Enable CosmosClientOptions with AllowDatabaseThroughputUpdate = true if scaling, but that’s side quest.
SDK Behaviors and Potential Bugs Causing False 404s
Not always your code. The .NET SDK v3 has quirks. Set EnableContentResponseOnWrite = true in ItemRequestOptions, and deletes throw 404 exceptions even on success (null response), as reported in this GitHub issue. Matches your trace: NotFound (404); Substatus: 0.
Your call lacks options, so default behavior. But if inherited from client, check. Workaround: Catch CosmosException, inspect StatusCode == HttpStatusCode.NotFound but Resource is populated? Nah, usually true miss.
Another: Cross-partition queries vs. single-partition deletes. Your feed works because GetItemQueryIterator fans out; delete routes direct. If PK value has invisible chars (portal hides 'em), 404.
Log diagnostics:
CosmosClientOptions options = new() { ApplicationName = "DeleteDiag" };
using var client = new CosmosClient(connectionString, options);
Check Azure Monitor for ActivityId a6858546-110a-45e6-8fec-e56ef7571b44—reveals routing target partition.
Another GitHub thread mirrors: Query OK, delete 404 on valid docs. Root? PK path ignorance.
Step-by-Step Verification in Azure Portal and Code
Don’t trust code alone. Portal first.
- Check container partition key: Data Explorer >
iothub> Scale & Settings. Note the path—/Body/partitionKey? Screenshot it. - Manual doc lookup: Query
SELECT * FROM c WHERE c.id = '6aa773ff-54cb-4837-9620-653d58a2bf5f'. Copy exactBody.partitionKeyvalue. Compare to your log. - Portal delete test: Right-click the doc > Delete. Succeeds? Then code issue. Fails? Doc changed/concurrency.
- ReadItemAsync test:
var readResponse = await container.ReadItemAsync<dynamic>(id, new PartitionKey(partitionKey));
if (readResponse.StatusCode == HttpStatusCode.OK) {
// PK good, proceed to delete
} else {
Debug.WriteLine($"Read failed: {readResponse.StatusCode}");
}
- Query top-level PK: Run
SELECT c.partitionKey, c.Body.partitionKey FROM c LIMIT 10. Null top-level? That’s your bug.
If portal delete works but code doesn’t, it’s string hell—copy-paste the portal value into code literal.
MS Docs confirm: Validate ID/PK exactly, no whitespace.
Optimized Code Solution and Best Practices
Full refactored method. Handles nesting, JSON safe, with Read pre-check. Swapped to strong-typed for speed (define a TelemetryDoc model).
public class TelemetryDoc {
public string id { get; set; }
public string partitionKey { get; set; } // Will map from Body.partitionKey
public dynamic Body { get; set; }
}
public async Task<int> DeleteDocumentsByDateRange(DateTime fromDate, DateTime toDate) {
int deletedCount = 0;
var options = new CosmosClientOptions { ApplicationName = "TelemetryDelete" };
using var client = new CosmosClient(connectionString, options);
var container = client.GetDatabase("telemetrydata").GetContainer("iothub");
var query = new QueryDefinition(@"
SELECT c.id, c.Body.partitionKey AS partitionKey
FROM c WHERE c.Body.TimeStamp >= @from AND c.Body.TimeStamp <= @to")
.WithParameter("@from", fromDate.ToString("O")) // Roundtrip ISO
.WithParameter("@to", toDate.ToString("O"));
var iterator = container.GetItemQueryIterator<TelemetryDoc>(query);
while (iterator.HasMoreResults) {
var response = await iterator.ReadNextAsync();
foreach (var doc in response) {
string id = doc.id?.Trim();
string pk = doc.partitionKey?.Trim();
if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(pk)) continue;
// Pre-verify with read
try {
var readResp = await container.ReadItemAsync<TelemetryDoc>(id, new PartitionKey(pk));
if (readResp.StatusCode != HttpStatusCode.OK) {
Debug.WriteLine($"Doc not readable: {id}/{pk}");
continue;
}
} catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) {
Debug.WriteLine($"Pre-read 404: {id}/{pk}");
continue;
}
// Delete
var deleteOpts = new ItemRequestOptions { ContentResponseOnWriteEnabled = false };
var deleteResp = await container.DeleteItemAsync<TelemetryDoc>(id, new PartitionKey(pk), deleteOpts);
if (deleteResp.StatusCode == HttpStatusCode.NoContent) {
deletedCount++;
} else {
Debug.WriteLine($"Delete {deleteResp.StatusCode}: {id}/{pk}");
}
}
}
return deletedCount;
}
For bulk (1000s docs), use TransactionalBatch—groups deletes in one RU-efficient call, avoids throttling.
Best practices:
- Model classes over
dynamic. - Batch size: 100 per transaction.
- Retry with exponential backoff on 429s.
- Monitor RUs in Metrics.
This should delete cleanly. Test small range first.
Sources
- Azure Cosmos DB .NET SDK Issue #3011 — SDK bug with EnableContentResponseOnWrite causing false 404 on deletes: https://github.com/Azure/azure-cosmos-dotnet-v3/issues/3011/
- Stack Overflow: Deleting all items from Cosmos DB in batch 404 — Partition key path mismatch in query projection example: https://stackoverflow.com/questions/62798778/deleting-all-items-from-cosmos-db-in-batch-404-not-found
- Azure Cosmos DB .NET SDK Issue #2742 — Query succeeds but DeleteItemAsync 404 on valid documents: https://github.com/Azure/azure-cosmos-dotnet-v3/issues/2742
- Troubleshoot 404 Resource Not Found in Azure Cosmos DB — Official guide on ID/partition key exact match requirements: https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/troubleshoot-not-found
- Stack Overflow: Resource not found when trying to delete from Cosmos DB — Partition key extraction and portal verification steps: https://stackoverflow.com/questions/53547561/resource-not-found-when-trying-to-delete-from-cosmos-db
- Stack Overflow: Delete an item using DeleteItemAsync with partition key — Handling ID/PK both as values in deletes: https://stackoverflow.com/questions/62893899/delete-an-item-using-deleteitemasync-when-partitionkeyvalue-and-id-both-values-a
- Troubleshoot Bad Request in Cosmos DB — Partition key header vs. document value mismatches: https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/troubleshoot-bad-request
Conclusion
Nail the cosmos db partition key path in your query projection—like c.Body.partitionKey for Azure Cosmos DB IoT containers—and those 404s vanish. Verify in portal, pre-read before delete, and lean on JObject or models for string safety. You’ll delete by date range smoothly, saving RUs and headaches. Got a massive backlog? TransactionalBatch next. Test it, tweak the path if your schema differs, and you’re golden.