--- a/src/protocol/pool_process_query.c
+++ b/src/protocol/pool_process_query.c
@@ -1951,6 +1951,7 @@
 	int			num_close_complete;
 	int			state;
 	bool		data_pushed;
+	bool		sent_sync = false;		/* true if we sent a Sync (not Flush) */
 
 	data_pushed = false;
 
@@ -2101,15 +2102,26 @@
 		/*
 		 * Send sync or flush message. If we are in an explicit transaction,
 		 * sending "sync" is safe because it will not break unnamed portal.
-		 * Also this is desirable because if no user queries are sent after
-		 * do_query(), COMMIT command could cause statement time out, because
-		 * flush message does not clear the alarm for statement time out which
-		 * has been set when do_query() issues query.
+		 *
+		 * In the extended-query path we now ALWAYS send Sync. The previous
+		 * code sent Flush when tstate != 'T', but tstate is only updated
+		 * from simple-query BEGIN/COMMIT CommandComplete tags; it is NOT
+		 * updated from the ReadyForQuery ('Z') transaction-status byte in
+		 * the extended path, and it is never 'T' for implicit (autocommit)
+		 * transactions. With Flush, the implicit transaction opened by the
+		 * backend around this Parse/Bind/Execute is never closed, leaving
+		 * the server connection "idle in transaction" after do_query()
+		 * returns. This is observable with clients that use the extended
+		 * protocol with implicit transactions (e.g. Quarkus/Agroal) behind
+		 * pgbouncer, and only when memory_cache_enabled = on, because that
+		 * is what makes pgpool issue these internal catalog lookups via
+		 * do_query() outside of an explicit transaction. Sync closes the
+		 * implicit transaction; the named statement/portal we use here
+		 * ("pgpool<PID>") is unaffected, and the unnamed portal is rebuilt
+		 * by the next user Parse anyway.
 		 */
-		if (backend->tstate == 'T')
-			pool_write(backend, "S", 1);	/* send "sync" message */
-		else
-			pool_write(backend, "H", 1);	/* send "flush" message */
+		pool_write(backend, "S", 1);		/* send "sync" message */
+		sent_sync = true;
 		len = htonl(sizeof(len));
 		pool_write_and_flush(backend, &len, sizeof(len));
 	}
@@ -2220,7 +2232,7 @@
 					return;
 
 				/* If "sync" message was issued, 'Z' is expected. */
-				if (doing_extended && backend->tstate == 'T')
+				if (doing_extended && sent_sync)
 					state |= COMMAND_COMPLETE_RECEIVED;
 				break;
 
@@ -2232,7 +2244,7 @@
 				 * If "sync" message was issued, 'Z' is expected, else we are
 				 * done with 'C'.
 				 */
-				if (!doing_extended || backend->tstate != 'T')
+				if (!doing_extended || !sent_sync)
 					state |= COMMAND_COMPLETE_RECEIVED;
 
 				/*
