diff --git a/parse/src/main/java/com/parse/ParseException.java b/parse/src/main/java/com/parse/ParseException.java
index 5dc3b5f90..6a6ff6b2b 100644
--- a/parse/src/main/java/com/parse/ParseException.java
+++ b/parse/src/main/java/com/parse/ParseException.java
@@ -102,6 +102,11 @@ public class ParseException extends Exception {
     public static final int FILE_DELETE_ERROR = 153;
     /** Error code indicating that the application has exceeded its request limit. */
     public static final int REQUEST_LIMIT_EXCEEDED = 155;
+    /**
+     * Error code indicating that the request was a duplicate and has been discarded due to
+     * idempotency rules.
+     */
+    public static final int DUPLICATE_REQUEST = 159;
     /** Error code indicating that the provided event name is invalid. */
     public static final int INVALID_EVENT_NAME = 160;
     /** Error code indicating that the username is missing or empty. */
diff --git a/parse/src/main/java/com/parse/ParseRESTCommand.java b/parse/src/main/java/com/parse/ParseRESTCommand.java
index 60cd1c590..20ac6ae12 100644
--- a/parse/src/main/java/com/parse/ParseRESTCommand.java
+++ b/parse/src/main/java/com/parse/ParseRESTCommand.java
@@ -20,6 +20,7 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.UUID;
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -33,6 +34,7 @@ class ParseRESTCommand extends ParseRequest<JSONObject> {
     /* package */ static final String HEADER_APP_BUILD_VERSION = "X-Parse-App-Build-Version";
     /* package */ static final String HEADER_APP_DISPLAY_VERSION = "X-Parse-App-Display-Version";
     /* package */ static final String HEADER_OS_VERSION = "X-Parse-OS-Version";
+    /* package */ static final String HEADER_REQUEST_ID = "X-Parse-Request-Id";
 
     /* package */ static final String HEADER_INSTALLATION_ID = "X-Parse-Installation-Id";
     /* package */ static final String USER_AGENT = "User-Agent";
@@ -49,6 +51,7 @@ class ParseRESTCommand extends ParseRequest<JSONObject> {
     /* package */ String httpPath;
     private String installationId;
     private String operationSetUUID;
+    private final String requestId = UUID.randomUUID().toString();
     private String localId;
 
     public ParseRESTCommand(
@@ -215,6 +218,7 @@ protected void addAdditionalHeaders(ParseHttpRequest.Builder requestBuilder) {
         if (masterKey != null) {
             requestBuilder.addHeader(HEADER_MASTER_KEY, masterKey);
         }
+        requestBuilder.addHeader(HEADER_REQUEST_ID, requestId);
     }
 
     @Override
diff --git a/parse/src/test/java/com/parse/ParseRESTCommandTest.java b/parse/src/test/java/com/parse/ParseRESTCommandTest.java
index 9bfbb7f50..743ea43ad 100644
--- a/parse/src/test/java/com/parse/ParseRESTCommandTest.java
+++ b/parse/src/test/java/com/parse/ParseRESTCommandTest.java
@@ -10,9 +10,11 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
@@ -30,6 +32,7 @@
 import java.io.InputStream;
 import java.net.URL;
 import java.util.Collections;
+import java.util.concurrent.atomic.AtomicReference;
 import org.json.JSONArray;
 import org.json.JSONObject;
 import org.junit.After;
@@ -552,4 +555,33 @@ public void testSaveObjectCommandUpdate() {
         ParsePlugins.reset();
         Parse.destroy();
     }
+
+    @Test
+    public void testIdempotencyLogic() throws Exception {
+        ParseHttpClient mockHttpClient = mock(ParseHttpClient.class);
+        AtomicReference<String> requestIdAtomicReference = new AtomicReference<>();
+        when(mockHttpClient.execute(
+                        argThat(
+                                argument -> {
+                                    assertNotNull(
+                                            argument.getHeader(ParseRESTCommand.HEADER_REQUEST_ID));
+                                    if (requestIdAtomicReference.get() == null)
+                                        requestIdAtomicReference.set(
+                                                argument.getHeader(
+                                                        ParseRESTCommand.HEADER_REQUEST_ID));
+                                    assertEquals(
+                                            argument.getHeader(ParseRESTCommand.HEADER_REQUEST_ID),
+                                            requestIdAtomicReference.get());
+                                    return true;
+                                })))
+                .thenThrow(new IOException());
+
+        ParseRESTCommand.server = new URL("http://parse.com");
+        ParseRESTCommand command = new ParseRESTCommand.Builder().build();
+        Task<Void> task = command.executeAsync(mockHttpClient).makeVoid();
+        task.waitForCompletion();
+
+        verify(mockHttpClient, times(ParseRequest.DEFAULT_MAX_RETRIES + 1))
+                .execute(any(ParseHttpRequest.class));
+    }
 }