Preventing duplicate posts: a practical guide to idempotency keys
How accidental double-posts actually happen — retry storms, double-clicks, timeouts — and how a stable Idempotency-Key and its 72-hour window stop them at the API.
Duplicate posts almost always come from retries — a client timeout followed by a blind retry, a retry storm after a crash, or a double-click — not from business-logic bugs. The fix is making retries safe: send a stable Idempotency-Key per logical publish on POST /api/posts; a repeated key returns the original post instead of creating a second one, honored for 72 hours.
A launch announcement going out twice on a customer's Instagram and LinkedIn is not a cosmetic bug — it's the kind of mistake that gets escalated. Duplicate posts almost never come from a bug in your business logic; they come from a network timeout, a retry storm, or an impatient double-click triggering a second request for a job that was already accepted the first time. The fix isn't trying to prevent every retry — it's making retries safe.
PostGate handles OAuth, publishing and delivery evidence for every network through one API. Start free or read the docs.
How do duplicate posts actually happen?
- Client-side timeout, blind retry. Your HTTP client gives up waiting for a response and retries the same call — but the original request may already have been accepted and queued. A client-side timeout only means you stopped waiting; it doesn't mean nothing happened.
- Retry storms. A queue worker or job runner retries a failed publish attempt without checking whether a previous attempt already succeeded, especially after a crash mid-job.
- Double-clicks and re-submits. A user clicks "Post now" twice, or a form re-submits after a page reload, firing two logically identical requests seconds apart.
How does an Idempotency-Key prevent duplicates?
PostGate accepts an Idempotency-Key header on every POST /api/posts call. If a request with that key has already been accepted, PostGate returns the original post instead of creating a second one — see Idempotency & safe retries. The key turns "did this already happen?" from a question your application has to answer into a question PostGate answers for you, on every retry, automatically.
curl -X POST "https://api.postgate.studio/api/posts?profile_group_id=grp_acme123" \
-H "Authorization: Bearer $POSTGATE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: launch-post-001" \
-d '{
"post": { "body": "Launch day. 🚀" },
"profiles": ["pro_instagram123", "pro_linkedin456"],
"media": ["https://cdn.example.com/launch.jpg"]
}'How should you generate the key?
A good idempotency key is stable for one logical publish and different for every distinct one — derived from your own internal job ID, queue message ID, or command ID, such as launch-post-{internal_job_id}. Generating a fresh random UUID on every retry defeats the entire mechanism: PostGate would see each attempt as a brand-new request and could publish more than once, which is the exact outcome you're trying to prevent.
How long does the key last, and what happens on a mismatch?
PostGate honors an idempotency key for 72 hours after it's first used. Retrying the identical request within that window returns the original post, unchanged — no new targets, no new attempts dispatched to any platform. Reusing the same key with different content — a different body, profiles, or media — is rejected with a 409 conflict instead of silently applying the new content, so a key collision can never quietly overwrite a post a customer already saw published.
{
"error": {
"code": "conflict",
"message": "This idempotency key was already used with a different request.",
"retryable": false,
"request_id": "req_01JXYZ"
}
}Does your UI still need its own guardrail?
A stable key protects the request that ultimately reaches PostGate, but it won't stop a double-click from firing two separate requests with two separate keys if your frontend doesn't also disable the submit control after the first click. Combine both: disable the button (or debounce the action) on your side, and derive a stable key from the underlying job so that even a request that slips through gets deduplicated at the API.
Frequently asked questions
What causes duplicate social media posts?
Three patterns cover almost all of them: a client-side timeout followed by a blind retry — the original request may already have been accepted, since a timeout only means you stopped waiting; a retry storm, where a queue worker retries a failed publish without checking whether a previous attempt already succeeded; and double-clicks or form re-submits firing two logically identical requests seconds apart.
How should I generate an idempotency key?
Derive it from your own internal job ID, queue message ID, or command ID — for example launch-post-{internal_job_id} — so it stays stable for one logical publish and differs for every distinct one. Generating a fresh random UUID on every retry defeats the entire mechanism: each attempt looks like a brand-new request and can publish more than once, which is exactly the outcome you are trying to prevent.
What happens if I reuse an idempotency key with different content?
The request is rejected with a 409 conflict instead of silently applying the new content, so a key collision can never quietly overwrite a post a customer already saw published. Retrying the identical request within the 72-hour window returns the original post, unchanged — no new targets, no new attempts dispatched to any platform.
Do idempotency keys stop double-clicks in my UI?
Not on their own. A stable key protects the request that reaches the API, but a double-click can fire two separate requests with two separate keys if your frontend does not disable the submit control after the first click. Combine both layers: disable the button or debounce the action on your side, and derive a stable key from the underlying job so even a request that slips through gets deduplicated.
