Three things could have happened
When your request times out, exactly one of three things happened.
It never reached the server. The server failed partway through. Or the server finished the work and the answer got lost coming back.
Your client cannot tell which. That ambiguity is the entire problem, because if it was the third one and the client retries, the operation runs twice.
Not retrying is not available
Do not reach for not retrying, because that is not available to you. Networks drop packets, servers restart during deploys, and a system that gives up on the first timeout is unusably flaky.
Expect retries from more directions than you would guess. HTTP clients with retry middleware built in. Queues that promise at-least-once delivery and redeliver anything whose consumer crashed before acknowledging. Webhook senders resending on timeout. And a user stabbing the pay button because the spinner looked stuck.
Give HTTP credit for trying to help, by declaring some methods safe to repeat by definition and one of them not. Reading twice is harmless, setting a field to seven twice leaves it at seven, deleting twice deletes once.
As intent rather than as a guarantee. The promise is only real if your handler honours it, and a supposedly safe method that appends to a log is not safe no matter what the method name says.
See where that leaves you. The operations that matter most, charge this card, create this order, ship this box, have no natural protection at all.
Take the conclusion, which falls out cleanly. Senders will retry and cannot know whether the first attempt landed, so your receiving side must recognise a repeat and refuse to apply it twice. The next two lessons are the two ways to build that.
Worked example
A food delivery app's mobile client retries checkout once after a 10-second timeout. That code sits harmlessly in the app for a year, because checkout normally takes 800 ms. Then one Friday night the payments service degrades and p99 latency climbs past 11 seconds, so the retry fires on a large share of orders while the slow first attempts are still completing. Result: 1,400 customers charged twice in three hours. Support burns the weekend issuing refunds, and the card processor flags the merchant account for an elevated dispute rate, which takes two weeks of emails to unwind. The retry wasn't the bug, and neither was the slowdown. The bug was a charge endpoint with no way to recognize the second request as a repeat of the first.