An integration creates an item in a SharePoint list. The request reaches SharePoint and the item is written, but the response never arrives: a connection drops, a gateway gives up, or the client stops waiting. The client retries, as it was built to, and the list now holds two items, possibly after two attempts that were both logged as failures.
Nothing malfunctioned. A client that did not receive the answer cannot know whether the write happened, and a retry is a second request unless something makes it the same one. HTTP says when a client may repeat a request on its own, Microsoft's retry guidance agrees, and several of the layers an integration is built on retry a POST without being asked. One item or two is decided when the create is designed, not when it fails.
This analysis is for integrations that write to SharePoint Online and Microsoft 365 through Microsoft Graph, the SharePoint REST API or Power Automate. The HTTP specifications and Microsoft's documentation were checked on 6 November 2026. No tenant was accessed; the one experiment below runs against a local stand-in.
What HTTP allows a client to repeat
RFC 9110 calls a method idempotent "if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request": PUT, DELETE and the safe methods, such as GET. POST is not, and PATCH "is neither safe nor idempotent" (RFC 5789). The rule for clients follows: "A client SHOULD NOT automatically retry a request with a non-idempotent method unless it has some means to know that the request semantics are actually idempotent, regardless of the method, or some means to detect that the original request was never applied."
A create is a POST because the server picks the identifier: "A service that selects a proper URI on behalf of the client, after receiving a state-changing request, SHOULD be implemented using the POST method rather than PUT" (RFC 9110, PUT). A SharePoint list item is created by posting its fields to the list (create a list item), and its id is read-only (listItem). Two POSTs, two identifiers.
The method on the wire is not always the operation. The SharePoint REST API sends updates and deletes as POST, with the operation named in an X-HTTP-Method header (basic REST operations). Our reading: a retry policy that decides by method alone cannot tell a SharePoint REST create from an update.
| Operation | Request | Same effect if repeated? |
|---|---|---|
| Read items, a file or a page of results | GET | Yes: safe and idempotent |
| Upload a file to a path you name | PUT to …/items/{parent-id}:/{filename}:/content | Yes in intent: the same bytes at the same path |
| Delete an item | DELETE, or POST with X-HTTP-Method: DELETE | Yes in intent; the second may answer 404 |
| Set field values on an item | PATCH to …/items/{item-id}/fields, or POST with X-HTTP-Method: MERGE | When it sets values; not when a retry recomputes them |
| Create a list item, a folder or an event; send a message | POST | No |
Sources for the table, checked on 6 November 2026: upload a file, update a list item, and Microsoft's web API implementation guidance, which notes that a repeated DELETE may return 404 and that "a PATCH operation that sets a property to a value can be idempotent, but one that increments a counter isn't."
Which failures leave the outcome unknown
After a failed write, the useful question is not whether the fault was transient but whether the write was applied. Microsoft's transient fault guidance calls 429 and 5xx responses "typical retry candidates". Its Retry pattern adds the condition: "Consider whether the operation is idempotent", because "a service might receive the request, process the request successfully, but fail to send a response."
| What the client received | What it establishes about the write | Send the same POST again? |
|---|---|---|
| 201, or another 2xx | Applied | No |
429 from Graph; 429, or 503 with Retry-After, from SharePoint | Throttled, and documented as failed | Yes, after waiting at least the Retry-After value |
| 400, 401, 403 or 404 | Refused, for a reason a retry does not fix | No: correct the request |
| A refusal of a keyed create, or 412 on a conditional update | The key is already taken, or the item changed since it was read, possibly by your own earlier attempt | Read first |
| 500, 502 or 504 | Not established. A 504 means a gateway "did not receive a timely response from an upstream server" | Only if the operation is idempotent or can be checked |
| A timeout, a reset connection, no response | Nothing | Only if the operation is idempotent or can be checked |
202 from Graph sendMail | Accepted, which "doesn't indicate that the request processing has completed" | No |
Sources, checked on 6 November 2026: Graph throttling, SharePoint throttling, transient fault handling, RFC 6585, 429, RFC 9110, 504 and sendMail.
Throttling is the reassuring row. Graph returns 429 "and the requests fail"; SharePoint returns 429 or 503 "and the requests will fail", with a Retry-After header in both cases. That is the "means to detect that the original request was never applied" that RFC 9110 asks for, so a throttled create can be sent again once the wait is over. SharePoint says "we require using the Retry-After HTTP header"; Graph adds exponential backoff where the header is absent, and warns that "all requests accrue against your usage limits". RFC 9110 lets the header carry either seconds or an HTTP date, so parse both.
The dangerous rows look like network problems. Of a 504, Microsoft's Dev Proxy guidance says: "The gateway gave up waiting, so you don't know whether the server did the work." Graph's error table says a 503 can be repeated "after a delay", not that the request went unapplied. Our reading: for a create without a key, treat a Graph 503 like the 5xx row.
The duplicate, reproduced without a tenant
The code below is illustrative, and it was run: a local stand-in written for this article, not SharePoint, executed with Python 3.15.0 on macOS on 6 November 2026. The endpoint stores each item, then answers after the client has stopped waiting. The client allows half a second per attempt, and two attempts. It runs once without a key and once with a key the endpoint will not store twice.
import json, threading, time, urllib.error, urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
stored, lock = [], threading.Lock()
class CreateEndpoint(BaseHTTPRequestHandler):
def do_POST(self):
item = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
with lock: # one atomic check-and-insert, as a unique column gives
if item.get("key") and any(s.get("key") == item["key"] for s in stored):
self.send_response(409)
self.end_headers()
return
stored.append(item) # the write is applied here...
time.sleep(1.5) # ...and the answer comes after the client gave up
self.send_response(201)
self.end_headers()
def log_message(self, *args):
pass
class Server(ThreadingHTTPServer):
def handle_error(self, request, client_address):
pass # the client has gone; the write it caused has not
def create(url, item, attempts=2, timeout=0.5):
seen = []
for _ in range(attempts):
request = urllib.request.Request(
url, json.dumps(item).encode(), method="POST",
headers={"Content-Type": "application/json"})
try:
seen.append(urllib.request.urlopen(request, timeout=timeout).status)
break
except urllib.error.HTTPError as error:
seen.append(error.code)
break
except (TimeoutError, urllib.error.URLError):
seen.append("timeout")
return seen
server = Server(("127.0.0.1", 0), CreateEndpoint)
threading.Thread(target=server.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{server.server_port}/items"
for label, item in [("no key", {"title": "Purchase request"}),
("with key", {"title": "Purchase request", "key": "op-7f3a"})]:
stored.clear()
seen = create(url, item)
time.sleep(2) # let the late answers finish before counting
print(f"{label}: client saw {seen}; server holds {len(stored)} item(s)")
server.shutdown()The observed output, identical in three runs:
no key: client saw ['timeout', 'timeout']; server holds 2 item(s)
with key: client saw ['timeout', 409]; server holds 1 item(s)Without a key, the client saw two failures and the endpoint holds two items. With a key, the second attempt is refused, and the refusal tells the client what it lacked: the first attempt was applied. Two variants we also ran break that result. A key generated afresh for each attempt left two items, and a check followed by a separate create let two concurrent writers both create. The run establishes how this client and this stand-in behave, and nothing about SharePoint's timings or error codes.
Where the second POST comes from
Most integrations never write their retry. They inherit it.
The .NET HTTP client. "By default, the standard resilience handler is configured to make retries for all HTTP methods," says Microsoft's resilience guidance, and "if a POST request inserts a new record to a database, then making retries for such a request could lead to data duplication." It retries 500 and above, 408, 429 and timeouts, up to three times within 30 seconds, with 10 seconds per attempt, and the same page shows ConfigureHttpClientDefaults adding it to every client. Our reading: a SharePoint create that takes more than ten seconds to answer is sent again while the first may still be completing. DisableForUnsafeHttpMethods() is too blunt a fix: it also stops the safe retry after a 429, and the retries of SharePoint REST updates sent as POST. Decide per operation and per outcome, in the retry strategy's ShouldHandle delegate.
The Microsoft Graph SDKs. They "already implement handlers that rely on the Retry-After header or default to an exponential backoff retry policy" (Graph throttling). Which status codes and methods the handler retries in a given SDK version is not stated on the Graph pages we read. Test it before relying on it for creates.
Power Automate. The default retry policy "sends up to two retries" on the Low performance profile, which includes Microsoft 365 plans, and "up to 12 retries" on Medium and High (limits of flows). The Logic Apps reference that page links to resends a request that "times out or fails, resulting in a 408, 429, or 5xx response". Our reading: a SharePoint create action that times out, or gets a 5xx, after the item was written creates it again by default. Turning the retry off also loses the safe retry after a 429; a key column, below, turns the repeat into a refusal instead.
Notification receivers. SharePoint resends a webhook notification whose delivery gets a status outside 200-299, "or that times out", 5 minutes later, up to 5 times (SharePoint webhooks), and Microsoft's reference implementation expects "an HTTP 200 message in under 5 seconds". Graph keeps retrying a change notification that is not acknowledged with a 2xx within 3 seconds, "for up to 4 hours" (Graph webhooks). Our reading: a receiver that writes before it answers, and answers late, writes again. Microsoft's advice is to "validate and persist the notification in a queue" and reply at once. That hands the problem to the queue's consumer, since most brokers deliver at least once, and the Idempotent Consumer pattern adds what is easiest to miss: "Making the message consumer idempotent doesn't protect the services it calls." The key has to travel on to SharePoint.
Layers on layers. RFC 9110 says "a client SHOULD NOT automatically retry a failed automatic retry", and the transient fault guidance does the arithmetic: "A retry with a count of three on both calls adds up to nine retry attempts in total against the service."
A person with a button. Our SharePoint Framework article offers "a way to try again" when a farm is busy, which is right for the reads it handles. On a write, that button is a retry too.
Making a create safe to repeat
In order of preference. These are our recommendations, built on what Microsoft documents.
1. Name the resource, and use PUT. Graph uploads a new file of up to 250 MB with a PUT to a path the client chooses (upload a file), and "the default for PUT is replace" (driveItem): the same bytes sent twice leave one file. The conflict setting for drive items also takes fail, under which a taken name is a conflict to investigate rather than an overwrite, and rename, the value in Microsoft's create-folder example, where the service "should choose a new name for the folder while creating it". Our reading: in an integration that retries, rename turns a lost response into a second folder.
2. Use the service's own key, where there is one. Events have transactionId, "a custom identifier specified by a client app for the server to avoid redundant POST operations in case of client retries to create the same event" (event). It matters twice over: "when you create an event that includes attendees, the server sends invitations to all attendees" (create event). For list items, folders and sendMail, the pages we read document no equivalent; the JSON body of sendMail takes the message and saveToSentItems, nothing more.
3. Otherwise, make SharePoint refuse the duplicate. Generate an operation key once, before the first attempt, store it with the work, and write it into a column with enforceUniqueValues: "If true, no two list items may have the same value for this column" (columnDefinition). The repeat is refused, and the refusal tells the client to read the item by its key and carry on. This is the constraint the Idempotent Consumer pattern recommends: "Use a uniqueness constraint on the deduplication key such that two transactions can attempt to insert a key, but only one can succeed." The key names the operation, not the attempt and not the conversation: the pattern warns against keys "derived from delivery attempts", as the run above shows, and, as our hybrid integration analysis puts it, "a correlation identifier used only in logs is not an idempotency mechanism". The error SharePoint returns for the violation is not stated on the pages we read: establish it on a test list first.
4. Check before creating, when one process owns each key. Read by key and create only if nothing comes back; filtering "works best on indexed columns" (list items). Alone, the check races, as the same pattern warns: "Both instances can pass the existence check before either instance commits, so the check alone doesn't prevent double processing." With more than one writer, put step 3 behind it.
5. For updates, send a condition. With if-match and a stale ETag, Graph says "a 412 Precondition Failed response is returned and the item will not be updated" (update a list item); without one, the SharePoint REST API "overwrites the item regardless of version" (lists and list items with REST). After a lost response, the retry can get a 412 because the first attempt succeeded, or a 2xx: RFC 9110 lets a server report success when "the change requested by the user agent has already succeeded, but the user agent might not be aware of it" (If-Match). Read before deciding. And retry the request, not the computation: a retry that re-reads and re-increments applies the increment twice.
A batch is not one request
Graph's JSON batching carries up to 20 requests, and "a 200 status code on the batch response headers doesn't indicate that the individual requests inside the batch succeeded". Throttled requests inside a batch fail one by one with 429; Graph says to "retry each failed request from the batch", and SDKs do not retry those automatically (Graph throttling). Our reading: re-sending the whole batch re-sends every create that succeeded, and a batch whose own response is lost leaves all its creates unknown. Retry the failed requests by id; settle the unknown ones by key.
What we recommend, in order
These are our recommendations, not Microsoft's.
- List every write, and every layer that retries it: the HTTP handler, the SDK, the flow action, the queue, notification redelivery and your own loops.
- Classify each write: repeatable by design, keyed, or neither.
- Give each write of the third kind a key, or a person. Either a key made once per operation and enforced by a unique column, or no automatic retry and an unknown outcome reported for someone to decide. Our newsletter makes the second choice, as the green test that measured nothing explains.
- Retry throttling for every write, after
Retry-After. After a 500, 502 or 504 or a timeout, resend a create only once a read by its key shows it missing; a create with no key is not resent. - Keep one retrying layer per call.
- Test the lost response. Let the server apply the write and answer after the client's timeout, then count what was stored. For the client alone, Microsoft's Dev Proxy fails real requests with the errors you choose; its advice is to check "that your app retries
GETrequests and sends eachPOSTonly once."
What this does not settle
- What Graph returns for a second create with the same
transactionId. The event pages we read do not say. - Which status codes and methods the Graph SDK retry handler covers in the version you ship. Test it.
- The error SharePoint returns when a create violates a unique column. Establish it on a test list.
- The default conflict setting for a POST that creates a folder, and where the setting goes. The create-folder page states no default, and Microsoft's pages disagree on placement: the driveItem page says the URL, the create-folder example uses the body.
- Whether a 500, 502 or 504 after a create means the item was written. No page we read says. Only a read by key settles it.
- Whether a repeated PUT of identical content adds a version in a library with versioning. RFC 9110 lets a server "retain a revision control history" for idempotent requests; the pages we read do not say what SharePoint records.
Where this fits
- A hybrid SharePoint integration needs more than a successful sign-in states the principle as a contract: "a retry is a second business request unless the contract says otherwise". This article supplies the HTTP rules and the SharePoint and Graph mechanisms that contract is built from.
- The last Graph page is not proof of tenant coverage covers retries of reads, where repeating is safe and the risk is mistaking an interrupted read for a complete one. Here the request is a write, and repeating it is the risk.
- Agents decide. Code executes. says a transaction "needs idempotency, a retry policy, a rollback path and a record that survives an audit". This is what the first two look like for a write to Microsoft 365.
- The green test that measured nothing has the specimen where a missing idempotency marker made every retry mail the whole list, and the at-least-once or at-most-once choice behind it.
- Sending email from an application, without a single password sends through Graph
sendMail. A retry of that call after a lost 202 is a second email. - Migrating privileged SharePoint server-side code asks "What happens twice?" of each elevated operation. Code that moves to remote APIs answers it with the patterns here.
Sources, checked on 6 November 2026
- HTTP: RFC 9110, HTTP Semantics (sections 9.2.2, 9.3.4, 10.2.3, 13.1.1 and 15.6.5), RFC 5789, PATCH, RFC 6585, 429 Too Many Requests
- Azure Architecture Center: Retry pattern, transient fault handling, Idempotent Consumer pattern, web API implementation
- .NET: build resilient HTTP apps
- Microsoft Graph: throttling, error responses, JSON batching, create a list item, listItem, list items, update a list item, columnDefinition, driveItem, create a folder, upload a file, event, create an event, sendMail, change notifications through webhooks
- SharePoint: avoid getting throttled or blocked, basic operations with REST, lists and list items with REST, webhooks overview, webhooks reference implementation
- Power Automate and Logic Apps: limits of automated, scheduled and instant flows, handle errors and exceptions in Azure Logic Apps
- Dev Proxy: 500, 502, 503 and 504 errors from APIs
Support independent engineering knowledge
Knowledge, Analysis, the Field Guide and the tools are free to read and use, and they stay that way. If this article saved you time, you can support the research and writing behind it. Support is voluntary and unlocks nothing.
Buy me a coffee (opens in a new tab)João Lívio's page on buymeacoffee.com