Updating a Microsoft Teams resource through Microsoft Graph looks straightforward until two operations try to change the same resource at nearly the same time.
A typical .NET application may read a Teams resource, change one property, and send an update back to Microsoft Graph. That works when the application is the only writer.
The situation changes when multiple users, services, automation jobs, or Teams clients modify the same resource concurrently.
For example:
Application A
|
| Read resource
|
v
Current state
Application B
|
| Update same resource
|
v
Resource changed
Application A
|
| Send update based on old state
v
ConflictA conflict means that the state your application used to build its update is no longer the same state held by the service.
Microsoft Graph uses HTTP status codes and response headers to communicate these situations. For supported APIs, optimistic concurrency can also use ETags and If-Match headers to prevent an application from overwriting a resource that changed after it was read.
The important lesson is that a concurrency conflict is not simply a request failure. It is information that the application must use to decide whether to retry, refresh the resource, merge the change, or ask the user to resolve the conflict.
Why Concurrent Updates Happen
Concurrent updates are common in Teams environments because several actors can interact with the same resource.
For example:
Teams client
|
+------+
|
Administrator --+--> Same Teams resource
|
Automation ----+
|
.NET service --+A resource might be changed by:
A Teams user
A Teams administrator
Another application
A background synchronization service
A scheduled automation job
Microsoft 365 service processes
The problem becomes visible when your application follows a read-modify-write pattern.
For example:
1. Read resource.
2. Change local object.
3. Send complete update.Between steps 1 and 3, another process may update the resource.
Your application is then working with stale data.
The Lost Update Problem
Consider a Teams resource containing:
Display name = Engineering
Description = Engineering teamApplication A reads it.
At the same time, Application B changes the description:
Display name = Engineering
Description = Engineering and Platform teamApplication A still has:
Display name = Engineering
Description = Engineering teamApplication A changes the display name:
Display name = Product Engineering
Description = Engineering teamIf Application A sends the entire object without concurrency protection, it may accidentally replace the newer description with the older value.
This is called a lost update.
The intended change was only the display name.
The unintended side effect was overwriting another application's newer description.
Optimistic Concurrency
The normal solution is optimistic concurrency.
The application does not lock the resource before reading it.
Instead, it reads the resource and records a version identifier, commonly represented by an ETag.
The workflow becomes:
Read resource
|
v
Receive ETag
|
v
Modify local state
|
v
Send update with If-Match
|
+---- Resource unchanged ---> Update succeeds
|
+---- Resource changed ----> ConflictThis is different from pessimistic locking.
With pessimistic locking, the application attempts to prevent other writers from changing the resource while it works.
With optimistic concurrency, the application assumes that conflicts are uncommon and checks whether the resource changed before committing the update.
For distributed cloud APIs, optimistic concurrency is generally more practical.
What an ETag Represents
An ETag is a version identifier associated with a particular representation of a resource.
Suppose the API returns:
ETag: "abc123"Your application stores that value along with the object.
Later, it sends:
If-Match: "abc123"The service can then determine whether the resource is still based on the same version.
If another operation changed the resource, the ETag no longer matches.
The service can reject the update instead of silently overwriting the newer state.
The exact concurrency behavior depends on the specific Microsoft Graph resource and operation, so developers should check the API documentation for the Teams endpoint they are using.
Why the Conflict Response Matters
One of the most important changes in a production application is how it interprets the response.
A concurrency conflict should not automatically trigger:
Retry
Retry
Retry
RetryThat can make the problem worse.
The correct behavior depends on why the conflict occurred and what your application is trying to change.
For example:
Conflict
|
+-- Refresh resource
|
+-- Compare changes
|
+-- Reapply safe change
|
+-- Retry once
|
+-- Ask user if changes overlapThe response therefore becomes part of the application's concurrency algorithm.
Start With the Actual API Contract
Before implementing conflict handling, identify the exact Teams resource and Graph operation.
“Teams update” is not one API.
Your application could be updating:
A team
A channel
A channel membership
A team member
A message
A tab
A meeting-related resource
A Teams app installation
Another Microsoft Graph resource associated with Teams
Each endpoint can have different update semantics.
Do not copy concurrency logic from one Teams endpoint and assume it applies to another.
Read the API contract for the exact operation your application uses.
Use Narrow Updates
One of the best ways to reduce concurrency problems is to update only what the application actually needs to change.
Suppose your application only changes a team's description.
Avoid building a workflow like:
Read entire team
↓
Modify local team object
↓
Send entire team objectwhen the API supports a targeted update.
Instead:
Read required state
↓
Modify description
↓
Send description changeNarrow updates reduce the number of properties that can accidentally overwrite changes made by another writer.
This is especially important when multiple systems share responsibility for the same Teams resource.
A Basic .NET Graph Update Pattern
A .NET application using Microsoft Graph might have a service method similar to:
public async Task UpdateTeamAsync(
string teamId,
string description,
CancellationToken cancellationToken)
{
var team = await graphClient.Teams[teamId]
.GetAsync(cancellationToken: cancellationToken);
if (team == null)
{
throw new InvalidOperationException(
"The Teams resource was not found.");
}
var update = new Team
{
Description = description
};
await graphClient.Teams[teamId]
.PatchAsync(
update,
cancellationToken: cancellationToken);
}This demonstrates the basic update operation, but it does not yet provide concurrency protection.
A production implementation should consider the versioning and conflict behavior documented for the specific Graph endpoint.
Do Not Blindly Retry 409 Responses
A common implementation mistake is:
for (var i = 0; i < 5; i++)
{
try
{
await UpdateAsync();
return;
}
catch
{
await Task.Delay(1000);
}
}This is dangerous.
If the failure is a real concurrency conflict, repeating the exact same stale update does not solve the problem.
The application must refresh its state first.
A better conceptual algorithm is:
Attempt update
|
v
Conflict?
|
Yes
|
v
Read latest resource
|
v
Recalculate intended update
|
v
Try againThe important step is recalculation.
Do not simply resend the same stale object.
Recalculate the Update After a Conflict
Suppose the user changes only the team description.
Your application originally read:
Name: Engineering
Description: Engineering teamAnother application changes:
Name: Engineering
Description: Engineering and Platform teamYour user still wants:
Description: Engineering delivery teamAfter detecting the conflict, refresh the resource:
Current resource:
Name: Engineering
Description: Engineering and Platform teamThen apply only the user's intended change:
Name: Engineering
Description: Engineering delivery teamThis preserves the latest name and other changes.
The important distinction is between:
Desired changeand:
Stale copy of the resourceYour application should retry the desired change, not the stale resource snapshot.
Build a Conflict-Aware Service
A clean service can separate these responsibilities.
public async Task UpdateDescriptionAsync(
string teamId,
string newDescription,
CancellationToken cancellationToken)
{
for (var attempt = 1; attempt <= 2; attempt++)
{
var current = await GetTeamAsync(
teamId,
cancellationToken);
try
{
await ApplyDescriptionUpdateAsync(
teamId,
current,
newDescription,
cancellationToken);
return;
}
catch (GraphServiceException ex)
when (IsConcurrencyConflict(ex) &&
attempt == 1)
{
continue;
}
}
throw new InvalidOperationException(
"The Teams resource changed while it was being updated.");
}The exact Graph exception and response handling should be implemented according to the Graph SDK version and endpoint being used.
The important design decision is the retry limit.
Join the conversation! Your thoughts help the community grow.