diff --git a/tornado/web.py b/tornado/web.py
index 8ff52e9c..d79889fa 100644
--- a/tornado/web.py
+++ b/tornado/web.py
@@ -993,6 +993,9 @@ class RequestHandler(object):
         self._log()
         self._finished = True
         self.on_finish()
+        self._break_cycles()
+
+    def _break_cycles(self):
         # Break up a reference cycle between this handler and the
         # _ui_module closures to allow for faster GC on CPython.
         self.ui = None
diff --git a/tornado/websocket.py b/tornado/websocket.py
index ce13d262..69437ee4 100644
--- a/tornado/websocket.py
+++ b/tornado/websocket.py
@@ -434,6 +434,16 @@ class WebSocketHandler(tornado.web.RequestHandler):
         if not self._on_close_called:
             self._on_close_called = True
             self.on_close()
+            self._break_cycles()
+
+    def _break_cycles(self):
+        # WebSocketHandlers call finish() early, but we don't want to
+        # break up reference cycles (which makes it impossible to call
+        # self.render_string) until after we've really closed the
+        # connection (if it was established in the first place,
+        # indicated by status code 101).
+        if self.get_status() != 101 or self._on_close_called:
+            super(WebSocketHandler, self)._break_cycles()
 
     def send_error(self, *args, **kwargs):
         if self.stream is None:

websocket demo exception
tornado/demos/websocket/chatdemo.py

```python
chat["html"] = tornado.escape.to_basestring(
            self.render_string("message.html", message=chat))
```
```
[E 170412 12:04:47 websocket:487] Uncaught exception in /chatsocket
    Traceback (most recent call last):
      File "/Users/crane/Projects/tornado/tornado/websocket.py", line 484, in _run_callback
        result = callback(*args, **kwargs)
      File "/Users/crane/Projects/tornado/demos/websocket/chatdemo.py", line 92, in on_message
        self.render_string("message.html", message=chat))
      File "/Users/crane/Projects/tornado/tornado/web.py", line 863, in render_string
        namespace = self.get_template_namespace()
      File "/Users/crane/Projects/tornado/tornado/web.py", line 887, in get_template_namespace
        namespace.update(self.ui)
    TypeError: 'NoneType' object is not iterable
```
diff --git a/tornado/test/websocket_test.py b/tornado/test/websocket_test.py
index 7bdca877..d47a74e6 100644
--- a/tornado/test/websocket_test.py
+++ b/tornado/test/websocket_test.py
@@ -8,6 +8,7 @@ from tornado.concurrent import Future
 from tornado import gen
 from tornado.httpclient import HTTPError, HTTPRequest
 from tornado.log import gen_log, app_log
+from tornado.template import DictLoader
 from tornado.testing import AsyncHTTPTestCase, gen_test, bind_unused_port, ExpectLog
 from tornado.test.util import unittest, skipBefore35, exec_test
 from tornado.web import Application, RequestHandler
@@ -130,6 +131,11 @@ class CoroutineOnMessageHandler(TestWebSocketHandler):
         self.write_message(message)
 
 
+class RenderMessageHandler(TestWebSocketHandler):
+    def on_message(self, message):
+        self.write_message(self.render_string('message.html', message=message))
+
+
 class WebSocketBaseTestCase(AsyncHTTPTestCase):
     @gen.coroutine
     def ws_connect(self, path, **kwargs):
@@ -168,7 +174,15 @@ class WebSocketTest(WebSocketBaseTestCase):
              dict(close_future=self.close_future)),
             ('/coroutine', CoroutineOnMessageHandler,
              dict(close_future=self.close_future)),
-        ])
+            ('/render', RenderMessageHandler,
+             dict(close_future=self.close_future)),
+        ], template_loader=DictLoader({
+            'message.html': '<b>{{ message }}</b>',
+        }))
+
+    def tearDown(self):
+        super(WebSocketTest, self).tearDown()
+        RequestHandler._template_loaders.clear()
 
     def test_http_request(self):
         # WS server, HTTP client.
@@ -219,6 +233,14 @@ class WebSocketTest(WebSocketBaseTestCase):
         self.assertEqual(response, u'hello \u00e9')
         yield self.close(ws)
 
+    @gen_test
+    def test_render_message(self):
+        ws = yield self.ws_connect('/render')
+        ws.write_message('hello')
+        response = yield ws.read_message()
+        self.assertEqual(response, '<b>hello</b>')
+        yield self.close(ws)
+
     @gen_test
     def test_error_in_on_message(self):
         ws = yield self.ws_connect('/error_in_on_message')

pip install -e .[dev]
parse_log_pytest
pytest
sphinx sphinx_rtd_theme pytest
apt-get update apt-get install -y gcc
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
Babel==2.11.0
certifi==2021.5.30
charset-normalizer==2.0.12
docutils==0.18.1
idna==3.10
imagesize==1.4.1
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
Jinja2==3.0.3
MarkupSafe==2.0.1
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
Pygments==2.14.0
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
pytz==2025.2
requests==2.27.1
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinx-rtd-theme==2.0.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
-e git+https://github.com/tornadoweb/tornado.git@ecd8968c5135b810cd607b5902dda2cd32122b39#egg=tornado
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
urllib3==1.26.20
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: tornado
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - babel==2.11.0
      - charset-normalizer==2.0.12
      - docutils==0.18.1
      - idna==3.10
      - imagesize==1.4.1
      - jinja2==3.0.3
      - markupsafe==2.0.1
      - pygments==2.14.0
      - pytz==2025.2
      - requests==2.27.1
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinx-rtd-theme==2.0.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jquery==4.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - urllib3==1.26.20
prefix: /opt/conda/envs/tornado

tornado/test/websocket_test.py::WebSocketTest::test_render_message
tornado/test/websocket_test.py::WebSocketTest::test_async_prepare tornado/test/websocket_test.py::WebSocketTest::test_bad_websocket_version tornado/test/websocket_test.py::WebSocketTest::test_binary_message tornado/test/websocket_test.py::WebSocketTest::test_check_origin_invalid tornado/test/websocket_test.py::WebSocketTest::test_check_origin_invalid_partial_url tornado/test/websocket_test.py::WebSocketTest::test_check_origin_invalid_subdomains tornado/test/websocket_test.py::WebSocketTest::test_check_origin_valid_no_path tornado/test/websocket_test.py::WebSocketTest::test_check_origin_valid_with_path tornado/test/websocket_test.py::WebSocketTest::test_client_close_reason tornado/test/websocket_test.py::WebSocketTest::test_coroutine tornado/test/websocket_test.py::WebSocketTest::test_error_in_on_message tornado/test/websocket_test.py::WebSocketTest::test_http_request tornado/test/websocket_test.py::WebSocketTest::test_path_args tornado/test/websocket_test.py::WebSocketTest::test_server_close_reason tornado/test/websocket_test.py::WebSocketTest::test_unicode_message tornado/test/websocket_test.py::WebSocketTest::test_websocket_callbacks tornado/test/websocket_test.py::WebSocketTest::test_websocket_close_buffered_data tornado/test/websocket_test.py::WebSocketTest::test_websocket_gen tornado/test/websocket_test.py::WebSocketTest::test_websocket_header_echo tornado/test/websocket_test.py::WebSocketTest::test_websocket_headers tornado/test/websocket_test.py::WebSocketTest::test_websocket_http_fail tornado/test/websocket_test.py::WebSocketTest::test_websocket_http_success tornado/test/websocket_test.py::WebSocketTest::test_websocket_network_fail tornado/test/websocket_test.py::WebSocketNativeCoroutineTest::test_native_coroutine tornado/test/websocket_test.py::NoCompressionTest::test_message_sizes tornado/test/websocket_test.py::ServerOnlyCompressionTest::test_message_sizes tornado/test/websocket_test.py::ClientOnlyCompressionTest::test_message_sizes tornado/test/websocket_test.py::DefaultCompressionTest::test_message_sizes tornado/test/websocket_test.py::PythonMaskFunctionTest::test_mask tornado/test/websocket_test.py::CythonMaskFunctionTest::test_mask tornado/test/websocket_test.py::ServerPeriodicPingTest::test_server_ping tornado/test/websocket_test.py::ClientPeriodicPingTest::test_client_ping tornado/test/websocket_test.py::MaxMessageSizeTest::test_large_message
tornado/web.py tornado/websocket.py
tornado/web.py tornado/websocket.py