diff --git a/layerindex/views.py b/layerindex/views.py
index 9f238fd..e7728ca 100644
--- a/layerindex/views.py
+++ b/layerindex/views.py
@@ -1671,7 +1671,6 @@ class TaskStatusView(TemplateView):
         return context
 
 def task_log_view(request, task_id):
-    from celery.result import AsyncResult
     if not request.user.is_authenticated:
         raise PermissionDenied
 
@@ -1679,7 +1678,11 @@ def task_log_view(request, task_id):
         # Block anything that looks like a path
         raise Http404
 
-    result = AsyncResult(task_id)
+    # The task records when it finished and its result in the Update. Read
+    # that before the log, which is complete by the time it is set. (The
+    # Celery result backend is no use here: with rpc:// only the process
+    # that started the task can read the result.)
+    updateobj = get_object_or_404(Update, task_id=task_id)
     start = int(request.GET.get('start', 0))
     try:
         f = open(os.path.join(settings.TASK_LOG_DIR, 'task_%s.log' % task_id), 'rb')
@@ -1693,24 +1696,13 @@ def task_log_view(request, task_id):
         datastr = re.sub(b'\n[^\n]+\r', b'\n', datastr)
         # We need to escape this or else things that look like tags in the output
         # will be interpreted as such by the browser
-        data = escape(datastr)
+        data = escape(datastr.decode('utf-8', errors='replace'))
         response = HttpResponse(data)
-        try:
-            ready = result.ready()
-        except ConnectionResetError:
-            # FIXME this shouldn't be happening so often, but ultimately we don't care -
-            # the frontend is polling so it'll likely succeed in a subsequent request
-            ready = False
-        if ready:
+        if updateobj.finished:
             response['Task-Done'] = '1'
-            updateobj = get_object_or_404(Update, task_id=task_id)
             response['Task-Duration'] = utils.timesince2(updateobj.started, updateobj.finished)
             response['Task-Progress'] = 100
-            if result.info:
-                if isinstance(result.info, dict):
-                    response['Task-Result'] = result.info.get('retcode', None)
-                else:
-                    response['Task-Result'] = -1
+            response['Task-Result'] = updateobj.retcode
         else:
             response['Task-Done'] = '0'
             preader = utils.ProgressReader(settings.TASK_LOG_DIR, task_id)
diff --git a/tests/test_update_button.py b/tests/test_update_button.py
index b518bc2..bb3b84a 100644
--- a/tests/test_update_button.py
+++ b/tests/test_update_button.py
@@ -60,3 +60,25 @@ def test_run_update_command_keeps_fields_set_by_command(settings, tmp_path, monk
     assert update.reload
     assert (update.log, update.retcode) == ('output', 0)
     assert update.finished is not None
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize('finished, retcode, done, result', [
+    (False, 0, '0', None),
+    (True, 0, '1', '0'),
+    (True, 2, '1', '2'),
+])
+def test_task_log_reports_status_from_update(client, django_user_model, settings, tmp_path,
+                                             finished, retcode, done, result):
+    settings.TASK_LOG_DIR = str(tmp_path)
+    (tmp_path / 'task_test-task.log').write_text('line 1 <b>\n', encoding='utf-8')
+    Update.objects.create(started=datetime.now(), task_id='test-task', retcode=retcode,
+                          finished=datetime.now() if finished else None)
+    client.force_login(django_user_model.objects.create_user('user', 'user@example.com', 'pw'))
+
+    response = client.get(reverse('task_log', args=('test-task',)))
+
+    assert response.status_code == 200
+    assert response.content == b'line 1 &lt;b&gt;\n'
+    assert response['Task-Done'] == done
+    assert response.get('Task-Result') == result
