diff --git a/python/vyos/utils/__init__.py b/python/vyos/utils/__init__.py
index 90620071b..3759b2125 100644
--- a/python/vyos/utils/__init__.py
+++ b/python/vyos/utils/__init__.py
@@ -1,32 +1,33 @@
 # Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
 # License as published by the Free Software Foundation; either
 # version 2.1 of the License, or (at your option) any later version.
 #
 # This library is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # Lesser General Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public
 # License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
 from vyos.utils import assertion
 from vyos.utils import auth
 from vyos.utils import boot
 from vyos.utils import commit
 from vyos.utils import configfs
 from vyos.utils import convert
 from vyos.utils import cpu
 from vyos.utils import dict
 from vyos.utils import file
 from vyos.utils import io
 from vyos.utils import kernel
 from vyos.utils import list
+from vyos.utils import locking
 from vyos.utils import misc
 from vyos.utils import network
 from vyos.utils import permission
 from vyos.utils import process
 from vyos.utils import system
diff --git a/python/vyos/utils/locking.py b/python/vyos/utils/locking.py
new file mode 100644
index 000000000..63cb1a816
--- /dev/null
+++ b/python/vyos/utils/locking.py
@@ -0,0 +1,115 @@
+# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+
+import fcntl
+import re
+import time
+from pathlib import Path
+
+
+class LockTimeoutError(Exception):
+    """Custom exception raised when lock acquisition times out."""
+
+    pass
+
+
+class InvalidLockNameError(Exception):
+    """Custom exception raised when the lock name is invalid."""
+
+    pass
+
+
+class Lock:
+    """Lock class to acquire and release a lock file"""
+
+    def __init__(self, lock_name: str) -> None:
+        """Lock class constructor
+
+        Args:
+            lock_name (str): Name of the lock file
+
+        Raises:
+            InvalidLockNameError: If the lock name is invalid
+        """
+        # Validate lock name
+        if not re.match(r'^[a-zA-Z0-9_\-]+$', lock_name):
+            raise InvalidLockNameError(f'Invalid lock name: {lock_name}')
+
+        self.__lock_dir = Path('/run/vyos/lock')
+        self.__lock_dir.mkdir(parents=True, exist_ok=True)
+
+        self.__lock_file_path: Path = self.__lock_dir / f'{lock_name}.lock'
+        self.__lock_file = None
+
+        self._is_locked = False
+
+    def __del__(self) -> None:
+        """Ensure the lock file is removed when the object is deleted"""
+        self.release()
+
+    @property
+    def is_locked(self) -> bool:
+        """Check if the lock is acquired
+
+        Returns:
+            bool: True if the lock is acquired, False otherwise
+        """
+        return self._is_locked
+
+    def __unlink_lockfile(self) -> None:
+        """Remove the lock file if it is not currently locked."""
+        try:
+            with self.__lock_file_path.open('w') as f:
+                fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
+                self.__lock_file_path.unlink(missing_ok=True)
+        except IOError:
+            # If we cannot acquire the lock, it means another process has it, so we do nothing.
+            pass
+
+    def acquire(self, timeout: int = 0) -> None:
+        """Acquire a lock file
+
+        Args:
+            timeout (int, optional): A time to wait for lock. Defaults to 0.
+
+        Raises:
+            LockTimeoutError: If lock could not be acquired within timeout
+        """
+        start_time: float = time.time()
+        while True:
+            try:
+                self.__lock_file = self.__lock_file_path.open('w')
+                fcntl.flock(self.__lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
+                self._is_locked = True
+                return
+            except IOError:
+                if timeout > 0 and (time.time() - start_time) >= timeout:
+                    if self.__lock_file:
+                        self.__lock_file.close()
+                    raise LockTimeoutError(
+                        f'Could not acquire lock within {timeout} seconds'
+                    )
+                time.sleep(0.1)
+
+    def release(self) -> None:
+        """Release a lock file"""
+        if self.__lock_file and self._is_locked:
+            try:
+                fcntl.flock(self.__lock_file, fcntl.LOCK_UN)
+                self._is_locked = False
+            finally:
+                self.__lock_file.close()
+                self.__lock_file = None
+                self.__unlink_lockfile()
diff --git a/src/helpers/vyos_net_name b/src/helpers/vyos_net_name
index 518e204f9..f5de182c6 100755
--- a/src/helpers/vyos_net_name
+++ b/src/helpers/vyos_net_name
@@ -1,257 +1,276 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2021-2024 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
 import re
 import time
 import logging
+import logging.handlers
 import tempfile
-import threading
+from pathlib import Path
 from sys import argv
 
 from vyos.configtree import ConfigTree
 from vyos.defaults import directories
 from vyos.utils.process import cmd
 from vyos.utils.boot import boot_configuration_complete
+from vyos.utils.locking import Lock
 from vyos.migrate import ConfigMigrate
 
+# Define variables
 vyos_udev_dir = directories['vyos_udev_dir']
-vyos_log_dir = '/run/udev/log'
-vyos_log_file = os.path.join(vyos_log_dir, 'vyos-net-name')
-
 config_path = '/opt/vyatta/etc/config/config.boot'
 
-lock = threading.Lock()
-
-try:
-    os.mkdir(vyos_log_dir)
-except FileExistsError:
-    pass
-
-logging.basicConfig(filename=vyos_log_file, level=logging.DEBUG)
 
 def is_available(intfs: dict, intf_name: str) -> bool:
-    """ Check if interface name is already assigned
-    """
+    """Check if interface name is already assigned"""
     if intf_name in list(intfs.values()):
         return False
     return True
 
+
 def find_available(intfs: dict, prefix: str) -> str:
-    """ Find lowest indexed iterface name that is not assigned
-    """
-    index_list = [int(x.replace(prefix, '')) for x in list(intfs.values()) if prefix in x]
+    """Find lowest indexed iterface name that is not assigned"""
+    index_list = [
+        int(x.replace(prefix, '')) for x in list(intfs.values()) if prefix in x
+    ]
     index_list.sort()
     # find 'holes' in list, if any
     missing = sorted(set(range(index_list[0], index_list[-1])) - set(index_list))
     if missing:
         return f'{prefix}{missing[0]}'
 
     return f'{prefix}{len(index_list)}'
 
+
 def mod_ifname(ifname: str) -> str:
-    """ Check interface with names eX and return ifname on the next format eth{ifindex} - 2
-    """
-    if re.match("^e[0-9]+$", ifname):
-        intf = ifname.split("e")
+    """Check interface with names eX and return ifname on the next format eth{ifindex} - 2"""
+    if re.match('^e[0-9]+$', ifname):
+        intf = ifname.split('e')
         if intf[1]:
             if int(intf[1]) >= 2:
-                return "eth" + str(int(intf[1]) - 2)
+                return 'eth' + str(int(intf[1]) - 2)
             else:
-               return "eth" + str(intf[1])
+                return 'eth' + str(intf[1])
 
     return ifname
 
+
 def get_biosdevname(ifname: str) -> str:
-    """ Use legacy vyatta-biosdevname to query for name
+    """Use legacy vyatta-biosdevname to query for name
 
     This is carried over for compatability only, and will likely be dropped
     going forward.
     XXX: This throws an error, and likely has for a long time, unnoticed
     since vyatta_net_name redirected stderr to /dev/null.
     """
     intf = mod_ifname(ifname)
 
     if 'eth' not in intf:
         return intf
     if os.path.isdir('/proc/xen'):
         return intf
 
     time.sleep(1)
 
     try:
         biosname = cmd(f'/sbin/biosdevname --policy all_ethN -i {ifname}')
     except Exception as e:
-        logging.error(f'biosdevname error: {e}')
+        logger.error(f'biosdevname error: {e}')
         biosname = ''
 
     return intf if biosname == '' else biosname
 
+
 def leave_rescan_hint(intf_name: str, hwid: str):
     """Write interface information reported by udev
 
     This script is called while the root mount is still read-only. Leave
     information in /run/udev: file name, the interface; contents, the
     hardware id.
     """
     try:
         os.mkdir(vyos_udev_dir)
     except FileExistsError:
         pass
     except Exception as e:
-        logging.critical(f"Error creating rescan hint directory: {e}")
+        logger.critical(f'Error creating rescan hint directory: {e}')
         exit(1)
 
     try:
         with open(os.path.join(vyos_udev_dir, intf_name), 'w') as f:
             f.write(hwid)
     except OSError as e:
-        logging.critical(f"OSError {e}")
+        logger.critical(f'OSError {e}')
+
 
 def get_configfile_interfaces() -> dict:
-    """Read existing interfaces from config file
-    """
+    """Read existing interfaces from config file"""
     interfaces: dict = {}
 
     if not os.path.isfile(config_path):
         # If the case, then we are running off of livecd; return empty
         return interfaces
 
     try:
         with open(config_path) as f:
             config_file = f.read()
     except OSError as e:
-        logging.critical(f"OSError {e}")
+        logger.critical(f'OSError {e}')
         exit(1)
 
     try:
         config = ConfigTree(config_file)
     except Exception:
         try:
-            logging.debug(f"updating component version string syntax")
+            logger.debug('updating component version string syntax')
             # this will update the component version string syntax,
             # required for updates 1.2 --> 1.3/1.4
             with tempfile.NamedTemporaryFile() as fp:
                 with open(fp.name, 'w') as fd:
                     fd.write(config_file)
                 config_migrate = ConfigMigrate(fp.name)
                 if config_migrate.syntax_update_needed():
                     config_migrate.update_syntax()
                     config_migrate.write_config()
                 with open(fp.name) as fd:
                     config_file = fd.read()
 
             config = ConfigTree(config_file)
 
         except Exception as e:
-            logging.critical(f"ConfigTree error: {e}")
+            logger.critical(f'ConfigTree error: {e}')
+            exit(1)
 
     base = ['interfaces', 'ethernet']
     if config.exists(base):
         eth_intfs = config.list_nodes(base)
         for intf in eth_intfs:
             path = base + [intf, 'hw-id']
             if not config.exists(path):
-                logging.warning(f"no 'hw-id' entry for {intf}")
+                logger.warning(f"no 'hw-id' entry for {intf}")
                 continue
             hwid = config.return_value(path)
             if hwid in list(interfaces):
-                logging.warning(f"multiple entries for {hwid}: {interfaces[hwid]}, {intf}")
+                logger.warning(
+                    f'multiple entries for {hwid}: {interfaces[hwid]}, {intf}'
+                )
                 continue
             interfaces[hwid] = intf
 
     base = ['interfaces', 'wireless']
     if config.exists(base):
         wlan_intfs = config.list_nodes(base)
         for intf in wlan_intfs:
             path = base + [intf, 'hw-id']
             if not config.exists(path):
-                logging.warning(f"no 'hw-id' entry for {intf}")
+                logger.warning(f"no 'hw-id' entry for {intf}")
                 continue
             hwid = config.return_value(path)
             if hwid in list(interfaces):
-                logging.warning(f"multiple entries for {hwid}: {interfaces[hwid]}, {intf}")
+                logger.warning(
+                    f'multiple entries for {hwid}: {interfaces[hwid]}, {intf}'
+                )
                 continue
             interfaces[hwid] = intf
 
-    logging.debug(f"config file entries: {interfaces}")
+    logger.debug(f'config file entries: {interfaces}')
 
     return interfaces
 
+
 def add_assigned_interfaces(intfs: dict):
-    """Add interfaces found by previous invocation of udev rule
-    """
+    """Add interfaces found by previous invocation of udev rule"""
     if not os.path.isdir(vyos_udev_dir):
         return
 
     for intf in os.listdir(vyos_udev_dir):
         path = os.path.join(vyos_udev_dir, intf)
         try:
             with open(path) as f:
                 hwid = f.read().rstrip()
         except OSError as e:
-            logging.error(f"OSError {e}")
+            logger.error(f'OSError {e}')
             continue
         intfs[hwid] = intf
 
+
 def on_boot_event(intf_name: str, hwid: str, predefined: str = '') -> str:
-    """Called on boot by vyos-router: 'coldplug' in vyatta_net_name
-    """
-    logging.info(f"lookup {intf_name}, {hwid}")
+    """Called on boot by vyos-router: 'coldplug' in vyatta_net_name"""
+    logger.info(f'lookup {intf_name}, {hwid}')
     interfaces = get_configfile_interfaces()
-    logging.debug(f"config file interfaces are {interfaces}")
+    logger.debug(f'config file interfaces are {interfaces}')
 
     if hwid in list(interfaces):
-        logging.info(f"use mapping from config file: '{hwid}' -> '{interfaces[hwid]}'")
+        logger.info(f"use mapping from config file: '{hwid}' -> '{interfaces[hwid]}'")
         return interfaces[hwid]
 
     add_assigned_interfaces(interfaces)
-    logging.debug(f"adding assigned interfaces: {interfaces}")
+    logger.debug(f'adding assigned interfaces: {interfaces}')
 
     if predefined:
         newname = predefined
-        logging.info(f"predefined interface name for '{intf_name}' is '{newname}'")
+        logger.info(f"predefined interface name for '{intf_name}' is '{newname}'")
     else:
         newname = get_biosdevname(intf_name)
-        logging.info(f"biosdevname returned '{newname}' for '{intf_name}'")
+        logger.info(f"biosdevname returned '{newname}' for '{intf_name}'")
 
     if not is_available(interfaces, newname):
         prefix = re.sub(r'\d+$', '', newname)
         newname = find_available(interfaces, prefix)
 
-    logging.info(f"new name for '{intf_name}' is '{newname}'")
+    logger.info(f"new name for '{intf_name}' is '{newname}'")
 
     leave_rescan_hint(newname, hwid)
 
     return newname
 
+
 def hotplug_event():
     # Not yet implemented, since interface-rescan will only be run on boot.
     pass
 
-if len(argv) > 3:
-    predef_name = argv[3]
-else:
-    predef_name = ''
-
-lock.acquire()
-if not boot_configuration_complete():
-    res = on_boot_event(argv[1], argv[2], predefined=predef_name)
-    logging.debug(f"on boot, returned name is {res}")
-    print(res)
-else:
-    logging.debug("boot configuration complete")
-lock.release()
+
+if __name__ == '__main__':
+    # Set up logging to syslog
+    syslog_handler = logging.handlers.SysLogHandler(address='/dev/log')
+    formatter = logging.Formatter(f'{Path(__file__).name}: %(message)s')
+    syslog_handler.setFormatter(formatter)
+
+    logger = logging.getLogger()
+    logger.addHandler(syslog_handler)
+    logger.setLevel(logging.DEBUG)
+
+    logger.debug(f'Started with arguments: {argv}')
+
+    if len(argv) > 3:
+        predef_name = argv[3]
+    else:
+        predef_name = ''
+
+    lock = Lock('vyos_net_name')
+    # Wait 60 seconds for other running scripts to finish
+    lock.acquire(60)
+
+    if not boot_configuration_complete():
+        res = on_boot_event(argv[1], argv[2], predefined=predef_name)
+        logger.debug(f'on boot, returned name is {res}')
+        print(res)
+    else:
+        logger.debug('boot configuration complete')
+
+    lock.release()
+    logger.debug('Finished')