diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c
index 652fa4a943d..77aec0fd9eb 100644
--- a/contrib/postgres_fdw/connection.c
+++ b/contrib/postgres_fdw/connection.c
@@ -112,6 +112,20 @@ static uint32 pgfdw_we_get_result = 0;
  */
 #define RETRY_CANCEL_TIMEOUT	1000
 
+/*
+ * Macro for constructing commit command to be sent
+ *
+ * We synchronize the read/write mode before committing remote transactions
+ * so deferred triggers on remote servers can run in the right mode.
+ */
+#define CONSTRUCT_COMMIT_COMMAND(sql, entry) \
+	do { \
+		if ((read_only_level > 0) && !(entry)->xact_read_only) \
+			strcpy((sql), "SET TRANSACTION READ ONLY; COMMIT TRANSACTION"); \
+		else \
+			strcpy((sql), "COMMIT TRANSACTION"); \
+	} while(0)
+
 /* Macro for constructing abort command to be sent */
 #define CONSTRUCT_ABORT_COMMAND(sql, entry, toplevel) \
 	do { \
@@ -942,7 +956,7 @@ begin_remote_xact(ConnCacheEntry *entry)
 			appendStringInfoString(&sql, "REPEATABLE READ");
 		if (ro)
 			appendStringInfoString(&sql, " READ ONLY");
-		if (XactDeferrable)
+		if (XactDeferrable && PQserverVersion(entry->conn) >= 90100)
 			appendStringInfoString(&sql, " DEFERRABLE");
 		entry->changing_xact_state = true;
 		do_sql_command(entry->conn, sql.data);
@@ -972,7 +986,7 @@ begin_remote_xact(ConnCacheEntry *entry)
 		if (entry->xact_depth == read_only_level)
 		{
 			entry->changing_xact_state = true;
-			do_sql_command(entry->conn, "SET transaction_read_only = on");
+			do_sql_command(entry->conn, "SET TRANSACTION READ ONLY");
 			entry->xact_read_only = true;
 			entry->changing_xact_state = false;
 		}
@@ -1005,7 +1019,7 @@ begin_remote_xact(ConnCacheEntry *entry)
 		initStringInfo(&sql);
 		appendStringInfo(&sql, "SAVEPOINT s%d", entry->xact_depth + 1);
 		if (ro)
-			appendStringInfoString(&sql, "; SET transaction_read_only = on");
+			appendStringInfoString(&sql, "; SET TRANSACTION READ ONLY");
 		entry->changing_xact_state = true;
 		do_sql_command(entry->conn, sql.data);
 		entry->xact_depth++;
@@ -1206,6 +1220,24 @@ pgfdw_xact_callback(XactEvent event, void *arg)
 	if (!xact_got_connection)
 		return;
 
+	/*
+	 * If we are called for pre-commit cleanup, ensure read_only_level is set
+	 * for later processing.  Note that we need to do this because the local
+	 * transaction may have become read-only since the last remote operation.
+	 */
+	if (event == XACT_EVENT_PARALLEL_PRE_COMMIT ||
+		event == XACT_EVENT_PRE_COMMIT)
+	{
+		if (XactReadOnly)
+		{
+			if (read_only_level == 0)
+				read_only_level = 1;
+			Assert(read_only_level == 1);
+		}
+		else
+			Assert(read_only_level == 0);
+	}
+
 	/*
 	 * Scan all connection cache entries to find open remote transactions, and
 	 * close them.
@@ -1222,6 +1254,8 @@ pgfdw_xact_callback(XactEvent event, void *arg)
 		/* If it has an open remote transaction, try to close it */
 		if (entry->xact_depth > 0)
 		{
+			char		sql[100];
+
 			elog(DEBUG3, "closing remote transaction on connection %p",
 				 entry->conn);
 
@@ -1237,14 +1271,17 @@ pgfdw_xact_callback(XactEvent event, void *arg)
 					pgfdw_reject_incomplete_xact_state_change(entry);
 
 					/* Commit all remote transactions during pre-commit */
+					CONSTRUCT_COMMIT_COMMAND(sql, entry);
 					entry->changing_xact_state = true;
 					if (entry->parallel_commit)
 					{
-						do_sql_command_begin(entry->conn, "COMMIT TRANSACTION");
+						do_sql_command_begin(entry->conn, sql);
 						pending_entries = lappend(pending_entries, entry);
 						continue;
 					}
-					do_sql_command(entry->conn, "COMMIT TRANSACTION");
+					do_sql_command(entry->conn, sql);
+					if ((read_only_level > 0) && !entry->xact_read_only)
+						entry->xact_read_only = true;
 					entry->changing_xact_state = false;
 
 					/*
@@ -2041,6 +2078,8 @@ pgfdw_finish_pre_commit_cleanup(List *pending_entries)
 	 */
 	foreach(lc, pending_entries)
 	{
+		char		sql[100];
+
 		entry = (ConnCacheEntry *) lfirst(lc);
 
 		Assert(entry->changing_xact_state);
@@ -2049,7 +2088,10 @@ pgfdw_finish_pre_commit_cleanup(List *pending_entries)
 		 * We might already have received the result on the socket, so pass
 		 * consume_input=true to try to consume it first
 		 */
-		do_sql_command_end(entry->conn, "COMMIT TRANSACTION", true);
+		CONSTRUCT_COMMIT_COMMAND(sql, entry);
+		do_sql_command_end(entry->conn, sql, true);
+		if ((read_only_level > 0) && !(entry)->xact_read_only)
+			entry->xact_read_only = true;
 		entry->changing_xact_state = false;
 
 		/* Do a DEALLOCATE ALL in parallel if needed */
diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml
index fe4e6478e28..01577d8d69b 100644
--- a/doc/src/sgml/postgres-fdw.sgml
+++ b/doc/src/sgml/postgres-fdw.sgml
@@ -1148,20 +1148,17 @@ CREATE SUBSCRIPTION my_subscription SERVER subscription_server PUBLICATION testp
   </para>
 
   <para>
-   The remote transaction is opened in the same read/write mode as the local
-   transaction: if the local transaction is <literal>READ ONLY</literal>,
-   the remote transaction is opened in <literal>READ ONLY</literal> mode,
-   otherwise it is opened in <literal>READ WRITE</literal> mode.
-   (This rule is also applied to remote and local subtransactions.)
+   Local <literal>READ ONLY</literal> transactions propagate their read-only
+   mode to remote sessions.
+   (This rule is also applied to local subtransactions.)
    Note that this does not prevent login triggers executed on the remote
    server from writing.
   </para>
 
   <para>
-   The remote transaction is also opened in the same deferrable mode as the
-   local transaction: if the local transaction is <literal>DEFERRABLE</literal>,
-   the remote transaction is opened in <literal>DEFERRABLE</literal> mode,
-   otherwise it is opened in <literal>NOT DEFERRABLE</literal> mode.
+   Also, local <literal>DEFERRABLE</literal> transactions propagate their
+   deferrable mode to remote sessions.
+   (This rule is only applied to remote servers 9.1 and newer.)
   </para>
 
   <para>
