Differences and Use Cases
Symlinks and hardlinks appear to solve the same problem, but they operate on entirely different levels of the filesystem. Understanding the difference at the inode level helps you avoid broken references, build reliable atomic deployments, and save significant disk space with rsync backups without risking data integrity.
Table of Contents
- 1. What fundamentally separates symlinks and hardlinks
- 2. How hardlinks work at the inode level
- 3. How symlinks work as path references
- 4. Why hardlinks cannot cross filesystem boundaries
- 5. Symlinks in atomic deployment: shared config and current
- 6. Hardlinks for space-efficient backups with rsync
- 7. Broken symlinks: causes, diagnosis and pitfalls
- 8. Permissions, ownership and security with links
- 9. Symlinks vs. hardlinks in direct comparison
- 10. Summary
- 11. FAQ
1. What fundamentally separates symlinks and hardlinks
In Linux filesystems such as ext4, XFS, or Btrfs, a file is technically not identical to its name in a directory. The actual content and its metadata, including size, permissions, timestamps, and block pointers, live in a structure called an inode. A directory entry is just a name pointing to an inode number. Both symlinks and hardlinks build on exactly this separation, but in fundamentally different ways.
A hardlink is a second directory entry that points to the very same inode as the original. There is no distinction between an original and a link, both are equal names for the same content. A symlink, by contrast, is its own tiny file with its own inode, whose content is nothing more than a path string to the target. Once this distinction is internalized, it immediately becomes clear why hardlinks carry restrictions that symlinks do not, and vice versa.
2. How hardlinks work at the inode level
The command ln source.txt link.txt, without the -s option, creates a new directory entry that points to exactly the same inode number as source.txt. Both names are then equivalent, neither is the "real" original. Every inode internally keeps a link count that is incremented with each new hardlink. Running stat -c '%i %h' file shows the inode number and link count directly, and ls -li displays the inode number as the first column in its output.
Deleting one of the two names with rm does not delete the content internally, it only decrements the link counter. Only once the counter reaches zero does the kernel actually free the data blocks. That also means: a change made through either name is immediately visible through the other, because they refer to exactly the same data blocks, not a copy.
# Inspect inode number and link count before and after a hardlink
$ ls -li notes.txt
1284712 -rw-r--r-- 1 deploy deploy 512 Jul 12 09:14 notes.txt
$ stat -c 'inode=%i links=%h' notes.txt
inode=1284712 links=1
# Create a hardlink, new directory entry, same inode
$ ln notes.txt notes-backup.txt
$ ls -li notes.txt notes-backup.txt
1284712 -rw-r--r-- 1 deploy deploy 512 Jul 12 09:14 notes-backup.txt
1284712 -rw-r--r-- 1 deploy deploy 512 Jul 12 09:14 notes.txt
$ stat -c 'inode=%i links=%h' notes.txt
inode=1284712 links=2
# Deleting one name only decrements the link counter
$ rm notes.txt
$ stat -c 'inode=%i links=%h' notes-backup.txt
inode=1284712 links=1
3. How symlinks work as path references
The command ln -s target.txt link.txt instead creates an entirely new file with its own inode number and its own file type l. The content of that file is exclusively the path string to the target, nothing more. When the symlink is accessed, the kernel resolves that path fresh at runtime, similar to a redirect. The path can be relative or absolute, and each choice has different consequences once directory structures are moved around.
A symlink formally has its own permission bits such as lrwxrwxrwx, but the kernel ignores them for the actual file access, what matters is the permission of the target. Unlike a hardlink, a symlink can point at a target that does not exist without ln -s itself throwing an error. That is convenient, because you can prepare references before the target exists, and risky, because such symlinks can silently become dead references.
# Create a symlink and inspect its own inode
$ ln -s /var/www/shared/config/env.php current-release/env.php
$ ls -li current-release/env.php
1299981 lrwxrwxrwx 1 deploy deploy 33 Jul 12 09:20 current-release/env.php -> /var/www/shared/config/env.php
# The symlink has its own inode, separate from the target
$ stat -c 'inode=%i type=%F' current-release/env.php
inode=1299981 type=symbolic link
# Resolve the final target path, following the chain if nested
$ readlink -f current-release/env.php
/var/www/shared/config/env.php
$ file current-release/env.php
current-release/env.php: symbolic link to /var/www/shared/config/env.php
4. Why hardlinks cannot cross filesystem boundaries
Inode numbers are only unique within a single filesystem instance. Every mounted filesystem, every partition, every network share, and every tmpfs maintains its own, independent inode table. A hardlink is, at its core, nothing more than the instruction "point this directory entry at inode X" within that same table. If inode X lives on a different device, that reference is meaningless, because the same number can denote something entirely different there. The kernel refuses the attempt with a clear error.
A symlink does not have this problem at all, because it merely stores a path string and resolves it fresh on every access. It does not care about device or mountpoint boundaries and works fine across partitions, network shares, and even container boundaries, as long as the path can be resolved at runtime. For setups with separate partitions for /var, /home, or mounted network storage, this is a decisive selection criterion.
In practice, the failed attempt looks like this: ln /mnt/backup/archive.tar.gz /srv/data/archive.tar.gz is rejected by the kernel with ln: failed to create hard link: Invalid cross-device link, provided /mnt/backup and /srv/data sit on different filesystems. The same command with -s works without restriction, because no inode reference across the device boundary is required.
5. Symlinks in atomic deployment: shared config and current
The classic pattern for atomic deployments, as implemented by Capistrano, Deployer, or custom deploy scripts, relies entirely on symlinks. Every release lands in its own directory releases/<timestamp>, while a single symlink named current points at whichever release is active. Configuration files and persistent data such as .env, uploaded media, or logs live centrally in shared/ and are linked into each new release, rather than being copied on every deploy.
The actual deploy switch happens with ln -sfn releases/20260712-1200 current. Because repointing a symlink internally runs through a single rename() system call, this step is atomic: there is no intermediate state where current points at nothing or only partially at the new release. Nginx or PHP-FPM see either the complete old release or the complete new release, never a mix. A rollback is equally trivial, the symlink is simply pointed back at the previous release directory.
# deploy-pipeline.yaml: atomic release switch via symlink
steps:
- name: Upload new release
run: rsync -az --delete build/ deploy@host:/var/www/releases/${RELEASE_ID}/
- name: Link shared resources into the new release
run: |
ssh deploy@host '
ln -sfn /var/www/shared/.env /var/www/releases/${RELEASE_ID}/.env
ln -sfn /var/www/shared/media /var/www/releases/${RELEASE_ID}/pub/media
ln -sfn /var/www/shared/var/log /var/www/releases/${RELEASE_ID}/var/log
'
- name: Atomic switch, single rename() syscall, zero downtime
run: ssh deploy@host 'ln -sfn /var/www/releases/${RELEASE_ID} /var/www/current'
- name: Reload PHP-FPM
run: ssh deploy@host 'sudo systemctl reload php8.4-fpm'
6. Hardlinks for space-efficient backups with rsync
For incremental backups, hardlinks are the tool of choice. With rsync --link-dest=previous-backup/, rsync only copies files that have actually changed since the last run, all unchanged files are instead created as hardlinks to the copy in the previous backup directory. The result is a complete, independently browsable directory tree per backup, even though only the files that actually changed consume additional disk space.
A local alternative without network transfer is cp -al, which duplicates an entire directory as a hardlink copy, ideal for quick snapshots before risky changes. One rule applies without exception: since all backup generations share the same inode and therefore the same data blocks, editing a file in place inside any single snapshot automatically changes it in every other snapshot too. Tools like rsync avoid that by consistently writing new files instead of in-place writes whenever content changes.
#!/usr/bin/env bash
set -euo pipefail
BACKUP_ROOT="/srv/backups/webshop"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
LATEST_LINK="${BACKUP_ROOT}/latest"
mkdir -p "${BACKUP_ROOT}/${TIMESTAMP}"
# Unchanged files become hardlinks to the previous backup,
# only genuinely changed files consume new disk space
rsync -a --delete \
--link-dest="${LATEST_LINK}" \
/var/www/current/ \
"${BACKUP_ROOT}/${TIMESTAMP}/"
# Point "latest" at the newest backup for the next incremental run
ln -sfn "${BACKUP_ROOT}/${TIMESTAMP}" "${LATEST_LINK}"
echo "[OK] Backup ${TIMESTAMP} complete"
du -sh "${BACKUP_ROOT}/${TIMESTAMP}"
7. Broken symlinks: causes, diagnosis and pitfalls
A broken symlink (also called a dangling reference) occurs as soon as the target is deleted, renamed, or moved to another filesystem while the symlink itself remains unchanged. This is especially common in Magento setups with a symlinked pub/media, in Docker volume mounts that have not yet been mounted, or in atomic deployments when the shared directory is created in the wrong order. Relative symlinks additionally break when they are resolved from a different working directory than originally intended.
For diagnosis, find /var/www -xtype l is the right tool, it specifically finds symlinks whose target no longer exists. In most terminals with colored ls output, broken symlinks additionally appear blinking or in red. What confuses many admins: ls -l on the symlink itself always works, because the directory entry does exist, only an actual access through the link returns No such file or directory. A recurring cron job with find -xtype l and alerting prevents such references from silently reaching production.
{
"scan_root": "/var/www/current",
"scanned_at": "2026-07-12T06:15:03+02:00",
"broken_symlinks_found": 2,
"entries": [
{
"path": "pub/media/catalog/product/placeholder.jpg",
"target": "/var/www/shared/media/catalog/product/placeholder.jpg",
"reason": "target_missing"
},
{
"path": "var/log/exception.log",
"target": "../shared/var/log/exception.log",
"reason": "relative_path_resolved_from_wrong_cwd"
}
],
"recommended_action": "run deploy-shared-init.sh before switching current symlink"
}
8. Permissions, ownership and security with links
The permission bits visibly formal on a symlink, such as lrwxrwxrwx, are meaningless to the kernel for actual file access, only the target's permissions matter. A chmod on a symlink by default even affects the target, not the link itself, which is why explicit tools with the -h or --no-dereference option are needed to actually change the link instead of accidentally its target. Ownership of the symlink entry itself only determines who may delete or replace it.
What matters for security are so-called symlink attacks in world-writable directories such as /tmp: an attacker replaces an expected file with a symlink to a sensitive target during a race-condition window between check and access. Using mktemp instead of fixed paths, along with opening with O_NOFOLLOW, reliably prevents that. Hardlinks carry a different risk: since a hardlink makes the same data accessible under a new name, it could theoretically be used to retain access to a file whose original path was removed. The kernel protection fs.protected_hardlinks has, since Linux 3.6, prevented users from creating hardlinks to files they do not own.
9. Symlinks vs. hardlinks in direct comparison
Choosing between a symlink and a hardlink is rarely a matter of taste, it almost always follows directly from the technical properties of each reference type. The table below summarizes the decisive differences for practical use.
| Property | Hardlink | Symlink | Practical impact |
|---|---|---|---|
| Across filesystem boundaries | Not possible (Invalid cross-device link) | Works fine, even across mountpoints | Separate partitions leave only the symlink option |
| Linking directories | Not allowed (prevents cycles in the tree) | Fully supported | shared/media as a directory only via symlink |
| Disk space for duplicates | No additional space (one inode) | Small additional reference data block | Backups with rsync --link-dest save massively |
| Atomic editor save (e.g. vim) | Reference breaks, editor creates a new inode | Survives, the path itself does not change | Config symlinks reliably survive editor saves |
| Target gets deleted | Data survives as long as link count > 0 | Becomes a broken symlink (dangling) | Symlinks need active monitoring on ordering |
In practice, both mechanisms complement each other rather than compete directly. Symlinks solve reference problems across directory and filesystem boundaries, for example shared configuration in deploy pipelines. Hardlinks solve disk space problems within a single filesystem, for example generational backups. Knowing both mechanisms lets you choose deliberately, instead of always reaching for ln -s out of habit just because it is the more familiar command.
Mironsoft
Deployment infrastructure, backup strategies and server automation
Reliable deployments and backups for your Magento stack?
We build atomic deploy pipelines with a clean symlink structure and set up space-efficient, version-safe backup routines with rsync and hardlinks for your infrastructure.
Deploy pipelines
Atomic releases with a releases/current symlink structure and shared config
Backup strategy
Hardlink-based backups with rsync --link-dest, set up version-safely
Monitoring
Automated checks for broken symlinks before every production switch
10. Summary
Symlinks vs. hardlinks is ultimately not a competition, but a question of matching the right mechanism to the right problem. Hardlinks share an inode and therefore the same data blocks, which means they only work within a single filesystem, never produce broken references, and save massive disk space in backups. Symlinks are standalone files with a path string as their content, work across filesystem and even network boundaries, can reference directories, and form the backbone of every atomic deployment mechanism built around a current symlink.
The decisive practical lever is using both mechanisms deliberately rather than out of habit: ln -s for shared configuration and atomic release switches, classic ln or rsync --link-dest for space-efficient backup generations. Anyone who additionally checks for broken symlinks on a regular basis before a new release goes live avoids the most common source of errors in symlink-based deploy pipelines.
Symlinks vs. Hardlinks: The Essentials at a Glance
Inode fundamentals
Hardlink = second name for the same inode. Symlink = its own inode whose content is a path string.
Filesystem boundaries
Hardlinks fail with Invalid cross-device link outside their own filesystem. Symlinks work everywhere.
Atomic deployments
ln -sfn releases/<id> current is a single rename() syscall, fully atomic, instant rollback possible.
Backups with hardlinks
rsync --link-dest saves disk space by hardlinking unchanged files to the previous backup.