diff --git a/arkindex/process/migrations/0061_workeractivity_updated_triggers.py b/arkindex/process/migrations/0061_workeractivity_updated_triggers.py
new file mode 100644
index 0000000000000000000000000000000000000000..65b6dd3877cae2f1c04de85ba801f8b1ce40af07
--- /dev/null
+++ b/arkindex/process/migrations/0061_workeractivity_updated_triggers.py
@@ -0,0 +1,50 @@
+# Generated by Django 4.0.7 on 2022-10-19 16:05
+
+from django.db import migrations, models
+
+import pgtrigger.compiler
+import pgtrigger.migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('process', '0060_remove_repository_provider_name'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='workeractivity',
+            name='updated',
+            field=models.DateTimeField(auto_now_add=True),
+        ),
+        pgtrigger.migrations.AddTrigger(
+            model_name='workeractivity',
+            trigger=pgtrigger.compiler.Trigger(
+                name='update_workeractivity_updated',
+                sql=pgtrigger.compiler.UpsertTriggerSql(
+                    func='NEW.updated = now(); RETURN NEW;',
+                    hash='1e5a8fa0718f420e6cd4f2a31434cd39a9c9bc67',
+                    operation='UPDATE',
+                    pgid='pgtrigger_update_workeractivity_updated_f2812',
+                    table='process_workeractivity',
+                    when='BEFORE',
+                )
+            ),
+        ),
+        pgtrigger.migrations.AddTrigger(
+            model_name='workeractivity',
+            trigger=pgtrigger.compiler.Trigger(
+                name='read_only_workeractivity_updated',
+                sql=pgtrigger.compiler.UpsertTriggerSql(
+                    condition='WHEN (OLD."updated" IS DISTINCT FROM (NEW."updated"))',
+                    func="RAISE EXCEPTION 'pgtrigger: Cannot update rows from % table', TG_TABLE_NAME;",
+                    hash='6276c6971a1d2669659e407418e2db1fa7dc6965',
+                    operation='UPDATE',
+                    pgid='pgtrigger_read_only_workeractivity_updated_a80ab',
+                    table='process_workeractivity',
+                    when='BEFORE',
+                )
+            ),
+        ),
+    ]
diff --git a/arkindex/process/models.py b/arkindex/process/models.py
index 7e49841faabad9e1ebd9288c89dc7424cd7f5379..31f0e008016e00c40cfdc999ded944c3a1a297ba 100644
--- a/arkindex/process/models.py
+++ b/arkindex/process/models.py
@@ -14,6 +14,7 @@ from django.utils.functional import cached_property
 from enumfields import Enum, EnumField
 from rest_framework.exceptions import ValidationError
 
+import pgtrigger
 from arkindex.documents.models import Element
 from arkindex.images.models import ImageServer
 from arkindex.process.managers import ActivityManager, CorpusWorkerVersionManager
@@ -867,13 +868,20 @@ class WorkerActivityState(Enum):
     Error = 'error'
 
 
-class WorkerActivity(IndexableModel):
+class WorkerActivity(models.Model):
     """
     Many-to-many relationship between Element and WorkerVersion
     Used to track the activity of a worker version among multiple elements
     """
     # Using an UUID helps to execute SQL raw INSERT
     id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
+
+    created = models.DateTimeField(auto_now_add=True)
+
+    # We will manually handle the timestamp update using a PostgreSQL trigger to avoid issues with bulk inserts/updates,
+    # and we still let Django handle setting the timestamp initially, so we use `auto_now_add` and not `auto_now`.
+    updated = models.DateTimeField(auto_now_add=True)
+
     element = models.ForeignKey(
         'documents.Element',
         on_delete=models.CASCADE,
@@ -921,6 +929,18 @@ class WorkerActivity(IndexableModel):
                 condition=Q(configuration__isnull=False),
             )
         ]
+        triggers = [
+            pgtrigger.Trigger(
+                name='update_workeractivity_updated',
+                operation=pgtrigger.Update,
+                when=pgtrigger.Before,
+                func='NEW.updated = now(); RETURN NEW;',
+            ),
+            pgtrigger.ReadOnly(
+                name='read_only_workeractivity_updated',
+                fields=['updated'],
+            )
+        ]
 
 
 class CorpusWorkerVersion(models.Model):
diff --git a/arkindex/process/tests/test_workeractivity.py b/arkindex/process/tests/test_workeractivity.py
index 4eeba62d3ffd26f46a3b131c4cade6a2d35dfa83..e24f1ccde81ec18f683597afb513ecedd5c7a64b 100644
--- a/arkindex/process/tests/test_workeractivity.py
+++ b/arkindex/process/tests/test_workeractivity.py
@@ -124,7 +124,7 @@ class TestWorkerActivity(FixtureTestCase):
         )
         for user, status_code, requests_count in cases:
             self.activity.state = WorkerActivityState.Queued
-            self.activity.save()
+            self.activity.save(update_fields=['state'])
             if user:
                 self.client.force_login(user)
             with self.assertNumQueries(requests_count):
diff --git a/arkindex/project/settings.py b/arkindex/project/settings.py
index 677b5c56d6c522f8c6efe70c8774b3b84231f7ff..7e79e251b4a23183f62293041f7cd2ed852fe789 100644
--- a/arkindex/project/settings.py
+++ b/arkindex/project/settings.py
@@ -109,6 +109,7 @@ INSTALLED_APPS = [
     'corsheaders',
     'django_rq',
     'drf_spectacular',
+    'pgtrigger',
     'ponos',
 
     # Our apps
diff --git a/requirements.txt b/requirements.txt
index ff2bc33dff3078595baba9de3f6f1e81371fd5c0..072b799b5a92ccec42ac8925b836807a4423653c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,6 +4,7 @@ bleach==5.0.1
 django-admin-hstore-widget==1.2.0
 django-cors-headers==3.13.0
 django-enumfields==2.1.1
+django-pgtrigger==4.6.0
 django-rq==2.5.1
 djangorestframework==3.12.4
 drf-spectacular==0.18.2