What Causes Device Replaced Errors in wacli and How Store Locking Prevents Them
Device replaced errors occur when multiple wacli instances simultaneously connect to the same WhatsApp device identity, and the store-level file lock prevents this by ensuring only one process can access the session at a time.
wacli is a command-line interface for WhatsApp built on the whatsmeow library. When users encounter "device replaced" or "session conflict" errors, it typically indicates concurrent access to the same device credentials stored in session.db. This article explains the root cause of these errors and how wacli's locking mechanism eliminates race conditions.
Why WhatsApp Reports "Device Replaced" Errors
The Single Connection Limit
WhatsApp's protocol allows only one active connection per device identity. The device identity is determined by the cryptographic keys stored in the session.db file within the store directory (default: ~/.wacli). When a client connects, WhatsApp associates that TCP connection with the specific key pair.
How Concurrent wacli Instances Trigger Conflicts
If two separate wacli processes attempt to use the same device identity simultaneously, the second login forces WhatsApp to drop the first connection. The server then reports a "device replaced" (or "session conflict") error, and the first process is disconnected. This behavior is documented in the project specification at docs/spec.md lines 45-48, which states that locking "prevents running multiple wacli instances against the same WhatsApp device identity, which can cause disconnects or 'device replaced' style failures."
How wacli Store Locking Prevents Device Replaced Errors
The File-Based Lock Implementation
wacli serializes all commands that need the WhatsApp session using a file-based lock stored at ~/.wacli/LOCK. The implementation in internal/lock/lock.go uses POSIX advisory locks via syscall.Flock:
// Open or create the lock file
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, err
}
Lock Acquisition and Fail-Fast Behavior
The Acquire function attempts to obtain an exclusive lock with non-blocking behavior:
// Acquire exclusive lock, fail immediately if held
err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err != nil {
return fmt.Errorf("store is locked (another wacli is running?): %w", err)
}
The LOCK_EX flag requests an exclusive lock, while LOCK_NB ensures the call returns immediately with an error if another process holds the lock. This fail-fast mechanism prevents the second instance from ever reaching the WhatsApp login step, eliminating the possibility of duplicate connections.
Lock Release on Process Termination
When wacli exits, the Release method in internal/lock/lock.go cleans up the lock:
func (l *Lock) Release() error {
// Release the advisory lock
if err := syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN); err != nil {
return err
}
return l.file.Close()
}
The LOCK_UN flag releases the exclusive lock, allowing subsequent wacli runs to proceed. The deferred release pattern ensures the lock is freed even if the process exits unexpectedly.
Implementing Store Locking in Your wacli Workflow
When building applications with wacli, always acquire the store lock before initializing the WhatsApp client. Here is the standard pattern used in internal/wa/client.go:
import (
"log"
"os"
"path/filepath"
"github.com/steipete/wacli/internal/lock"
)
func main() {
// Determine store directory
storeDir, _ := os.UserHomeDir()
storeDir = filepath.Join(storeDir, ".wacli")
// -------------------------------------------------
// 1️⃣ Acquire the exclusive lock
// -------------------------------------------------
lk, err := lock.Acquire(storeDir)
if err != nil {
// Most common cause: another wacli instance is already running
log.Fatalf("cannot start wacli: %v", err)
}
// Ensure the lock is released when we exit
defer func() {
if err := lk.Release(); err != nil {
log.Printf("failed to release lock: %v", err)
}
}()
// -------------------------------------------------
// 2️⃣ Proceed with WhatsApp operations
// -------------------------------------------------
// ... initialize whatsmeow client, sync messages, etc.
}
If you attempt to run a second instance while the first holds the lock, you will see the fail-fast error:
lk, err := lock.Acquire(dir)
if err != nil {
// Output: store is locked (another wacli is running?): resource temporarily unavailable
// (pid=12345, acquired_at=2023-08-01T12:34:56Z)
}
The test suite in internal/lock/lock_test.go verifies this behavior by spawning a helper process that attempts to acquire the lock while it is held, confirming that the second caller receives the "store is locked" error.
Summary
- Device replaced errors occur when multiple
wacliprocesses connect to WhatsApp using the same device identity keys stored insession.db. - WhatsApp permits only one active connection per device, so a second login forces the first to disconnect with a "device replaced" or "session conflict" error.
wacliprevents concurrent access using a file-based store lock at~/.wacli/LOCKimplemented ininternal/lock/lock.go.- The
lock.Acquire()function usessyscall.FlockwithLOCK_EX|LOCK_NBto obtain an exclusive lock immediately or fail fast, preventing duplicate connections before they occur. - The lock is released automatically via
lock.Release()when the process exits, ensuring subsequent runs can proceed without manual intervention.
Frequently Asked Questions
What does "device replaced" mean in wacli?
The "device replaced" error indicates that the WhatsApp server has disconnected your wacli session because another client logged in using the same device identity keys. This typically happens when two wacli instances attempt to run simultaneously using the same session.db file, causing the second connection to supersede the first.
Can I run multiple wacli instances with different phone numbers?
Yes, you can run multiple wacli instances concurrently as long as each uses a separate store directory. Each phone number requires its own identity keys and session.db. Specify different paths using the --store flag or environment variables so that each instance maintains its own LOCK file and never conflicts with others.
What happens if wacli crashes while holding the lock?
If wacli crashes or terminates without calling lock.Release(), the advisory lock (syscall.Flock) is automatically released by the operating system when the file descriptor closes. However, if the lock file itself remains, subsequent wacli runs will recreate and lock it normally. The lock is purely advisory and tied to the process lifecycle, so stale locks from dead processes do not block new instances indefinitely.
How do I manually clear a stale lock file?
Under normal circumstances, you do not need to manually delete the lock file because the operating system releases advisory locks when processes exit. However, if you encounter a "store is locked" error and are certain no other wacli process is running, you can safely remove the LOCK file from your store directory (default: ~/.wacli/LOCK). Ensure no other process is actively using the directory before deletion to avoid data corruption.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →