thanos-io/thanos · error
hard link file
Error message
hard link file %s
What it means
hardlinkBlock hardlinks every chunk file plus meta.json and index.json from the source block into the staging directory using os.Link. If linking any individual file fails, the upload aborts with "hard link file <name>". Hardlinks require both paths on the same filesystem, so this commonly fails when the staging/temp dir and the block data dir live on different mounts.
Solutions
- Ensure the shipper's local data directory and its temporary/staging directory are on the same filesystem (hardlinks cannot cross mounts — EXDEV).
- Check the named file still exists in the source block; if it vanished, another process (compaction/cleanup) is racing the shipper — exclude the dir or pause it.
- Verify write permission on the destination staging directory and read permission on the source block.
- If the filesystem does not support hardlinks (some NFS/FUSE/CIFS setups), configure Thanos to avoid hardlinking or move data onto a POSIX filesystem.
Example fix
// before: temp dir on a different volume than data dir --tsdb.path=/data --tmp.dir=/tmp // after: same filesystem so os.Link succeeds --tsdb.path=/data --tmp.dir=/data/tmp
Defensive patterns
Strategy: validation
Validate before calling
srcFi, _ := os.Stat(dataDir)
dstFi, _ := os.Stat(tmpDir)
if !sameDevice(srcFi, dstFi) {
return fmt.Errorf("data dir and tmp dir must be on the same filesystem for hardlinks")
}
func sameDevice(a, b os.FileInfo) bool { return a.Sys().(*syscall.Stat_t).Dev == b.Sys().(*syscall.Stat_t).Dev } Try / catch
var pathErr *os.LinkError
if errors.As(err, &pathErr) {
level.Error(logger).Log("msg", "hardlink failed (cross-device?)", "err", err)
} Prevention
- Keep the TSDB data dir and any temp/staging dir on the same filesystem/mount.
- In containers, mount both paths from the same volume, not separate volume types.
- Do not run compaction/cleanup concurrently with shipping on the same directory.
- Prefer local POSIX filesystems over NFS/FUSE that may not support hardlinks.
When it happens
Trigger: Sync()/upload() calling os.Link(src/<fn>, dst/<fn>) for a chunk file, meta.json, or index.json when the link fails — cross-device link, missing source file, or permissions.
Common situations: Data dir and temp/staging dir on different filesystems or Docker volumes (EXDEV: invalid cross-device link); chunk file deleted by concurrent compaction mid-upload; read-only or permission-restricted source; disk full on destination (though hardlinks themselves need no space, the earlier mkdir would).
Understand the failure class
Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.
Related errors
AI-assisted analysis of thanos-io/thanos@35b8b99117 (2026-09-07).
Data as JSON: /api/errors/cf1e3c2e7cf838e9.
Report an issue: GitHub.
Appendix: source
Thrown at pkg/shipper/shipper.go:601
return errors.Wrap(err, "create chunks dir")
}
fis, err := os.ReadDir(filepath.Join(src, block.ChunksDirname))
if err != nil {
return errors.Wrap(err, "read chunk dir")
}
files := make([]string, 0, len(fis))
for _, fi := range fis {
files = append(files, fi.Name())
}
for i, fn := range files {
files[i] = filepath.Join(block.ChunksDirname, fn)
}
files = append(files, block.MetaFilename, block.IndexFilename)
for _, fn := range files {
if err := os.Link(filepath.Join(src, fn), filepath.Join(dst, fn)); err != nil {
return errors.Wrapf(err, "hard link file %s", fn)
}
}
return nil
}
// Meta defines the format thanos.shipper.json file that the shipper places in the data directory.
type Meta struct {
Version int `json:"version"`
Uploaded []ulid.ULID `json:"uploaded"`
}
const (
// DefaultMetaFilename is the default JSON filename for meta information.
DefaultMetaFilename = "thanos.shipper.json"
// MetaVersion1 represents 1 version of meta.
MetaVersion1 = 1
)View on GitHub (pinned to 35b8b99117)