diff --git a/src/cal_tools/tools.py b/src/cal_tools/tools.py
index 2d15866ac2517bc0925e2b6ad5bad10560f5ab36..ee54ffa826d6f571fb588c274163af9079530ea9 100644
--- a/src/cal_tools/tools.py
+++ b/src/cal_tools/tools.py
@@ -4,6 +4,7 @@ import os
 import re
 import zlib
 from collections import OrderedDict
+from fcntl import lockf, LOCK_EX, LOCK_UN
 from glob import glob
 from multiprocessing.pool import ThreadPool
 from os import environ, listdir, path
@@ -811,6 +812,35 @@ def module_index_to_qm(index: int, total_modules: int = 16):
     return f"Q{quad+1}M{mod+1}"
 
 
+def recursive_update(target: dict, source: dict):
+    for k, v2 in source.items():
+        v1 = target.get(k, None)
+        if isinstance(v1, dict) and isinstance(v2, dict):
+            recursive_update(v1, v2)
+        else:
+            target[k] = v2
+
+
+def update_metadata(folder, changes: dict):
+    yaml_fn = Path(folder) / "calibration_metadata.yml"
+    try:
+        f = yaml_fn.open('x')
+    except FileExistsError:
+        f = yaml_fn.open('r+')
+
+    with f:
+        lockf(f, LOCK_EX)
+        d = yaml.safe_load(f)
+        if d is None:
+            d = {}
+
+        recursive_update(d, changes)
+
+        f.seek(0)
+        yaml.safe_dump(d, f)
+        f.truncate()
+
+
 class CalibrationMetadata(dict):
     """Convenience class: dictionary stored in metadata YAML file