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/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 3aab56b0642..6c87a27243b 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -13373,6 +13373,7 @@ DROP VIEW my_application_name;
 -- test read-only and/or deferrable transactions
 -- ===================================================================
 CREATE TABLE loct (f1 int, f2 text);
+INSERT INTO loct VALUES (1, 'foo'), (2, 'bar');
 CREATE FUNCTION locf() RETURNS SETOF loct LANGUAGE SQL AS
   'UPDATE public.loct SET f2 = f2 || f2 RETURNING *';
 CREATE VIEW locv AS SELECT t.* FROM locf() t;
@@ -13380,7 +13381,6 @@ CREATE FOREIGN TABLE remt (f1 int, f2 text)
   SERVER loopback OPTIONS (table_name 'locv');
 CREATE FOREIGN TABLE remt2 (f1 int, f2 text)
   SERVER loopback2 OPTIONS (table_name 'locv');
-INSERT INTO loct VALUES (1, 'foo'), (2, 'bar');
 START TRANSACTION READ ONLY;
 SAVEPOINT s;
 SELECT * FROM remt;  -- should fail
@@ -13469,9 +13469,32 @@ ERROR:  cannot execute UPDATE in a read-only transaction
 CONTEXT:  SQL function "locf" statement 1
 remote SQL command: SELECT f1, f2 FROM public.locv
 ROLLBACK;
+-- Clean up
 DROP FOREIGN TABLE remt;
+DROP FOREIGN TABLE remt2;
+DROP VIEW locv;
+DROP FUNCTION locf();
 CREATE FOREIGN TABLE remt (f1 int, f2 text)
   SERVER loopback OPTIONS (table_name 'loct');
+CREATE FUNCTION defer_trig_func() RETURNS TRIGGER LANGUAGE plpgsql AS $$
+BEGIN
+  IF NEW.f2 IS NOT NULL THEN
+    UPDATE public.loct SET f2 = f2 || f2 WHERE f1 = NEW.f1;
+  END IF;
+    RETURN NULL;
+END;
+$$;
+CREATE CONSTRAINT TRIGGER defer_trig AFTER INSERT ON loct
+  DEFERRABLE INITIALLY DEFERRED
+  FOR EACH ROW EXECUTE PROCEDURE defer_trig_func();
+START TRANSACTION;
+INSERT INTO remt VALUES (3, 'baz');
+SET TRANSACTION READ ONLY;
+COMMIT;
+ERROR:  cannot execute UPDATE in a read-only transaction
+CONTEXT:  SQL statement "UPDATE public.loct SET f2 = f2 || f2 WHERE f1 = NEW.f1"
+PL/pgSQL function public.defer_trig_func() line 4 at SQL statement
+remote SQL command: SET TRANSACTION READ ONLY; COMMIT TRANSACTION
 START TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY;
 SELECT * FROM remt;
  f1 | f2  
@@ -13501,9 +13524,8 @@ SELECT * FROM remt;
 COMMIT;
 -- Clean up
 DROP FOREIGN TABLE remt;
-DROP FOREIGN TABLE remt2;
-DROP VIEW locv;
-DROP FUNCTION locf();
+DROP TRIGGER defer_trig ON loct;
+DROP FUNCTION defer_trig_func;
 DROP TABLE loct;
 -- ===================================================================
 -- test parallel commit and parallel abort
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 9c271953206..0b313ebb418 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -4659,6 +4659,8 @@ DROP VIEW my_application_name;
 -- test read-only and/or deferrable transactions
 -- ===================================================================
 CREATE TABLE loct (f1 int, f2 text);
+INSERT INTO loct VALUES (1, 'foo'), (2, 'bar');
+
 CREATE FUNCTION locf() RETURNS SETOF loct LANGUAGE SQL AS
   'UPDATE public.loct SET f2 = f2 || f2 RETURNING *';
 CREATE VIEW locv AS SELECT t.* FROM locf() t;
@@ -4666,7 +4668,6 @@ CREATE FOREIGN TABLE remt (f1 int, f2 text)
   SERVER loopback OPTIONS (table_name 'locv');
 CREATE FOREIGN TABLE remt2 (f1 int, f2 text)
   SERVER loopback2 OPTIONS (table_name 'locv');
-INSERT INTO loct VALUES (1, 'foo'), (2, 'bar');
 
 START TRANSACTION READ ONLY;
 SAVEPOINT s;
@@ -4712,10 +4713,32 @@ SET transaction_read_only = on;
 SELECT * FROM remt2;  -- should fail
 ROLLBACK;
 
+-- Clean up
 DROP FOREIGN TABLE remt;
+DROP FOREIGN TABLE remt2;
+DROP VIEW locv;
+DROP FUNCTION locf();
+
 CREATE FOREIGN TABLE remt (f1 int, f2 text)
   SERVER loopback OPTIONS (table_name 'loct');
 
+CREATE FUNCTION defer_trig_func() RETURNS TRIGGER LANGUAGE plpgsql AS $$
+BEGIN
+  IF NEW.f2 IS NOT NULL THEN
+    UPDATE public.loct SET f2 = f2 || f2 WHERE f1 = NEW.f1;
+  END IF;
+    RETURN NULL;
+END;
+$$;
+CREATE CONSTRAINT TRIGGER defer_trig AFTER INSERT ON loct
+  DEFERRABLE INITIALLY DEFERRED
+  FOR EACH ROW EXECUTE PROCEDURE defer_trig_func();
+
+START TRANSACTION;
+INSERT INTO remt VALUES (3, 'baz');
+SET TRANSACTION READ ONLY;
+COMMIT;
+
 START TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY;
 SELECT * FROM remt;
 COMMIT;
@@ -4730,9 +4753,8 @@ COMMIT;
 
 -- Clean up
 DROP FOREIGN TABLE remt;
-DROP FOREIGN TABLE remt2;
-DROP VIEW locv;
-DROP FUNCTION locf();
+DROP TRIGGER defer_trig ON loct;
+DROP FUNCTION defer_trig_func;
 DROP TABLE loct;
 
 -- ===================================================================
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>
