# What Causes Device Replaced Errors in wacli and How Store Locking Prevents Them

> Learn what causes device replaced errors in wacli and how store locking prevents multiple instances from corrupting your WhatsApp device identity. Ensure seamless operation.

- Repository: [Peter Steinberger/wacli](https://github.com/steipete/wacli)
- Tags: internals
- Published: 2026-04-17

---

**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`](https://github.com/steipete/wacli/blob/main/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`](https://github.com/steipete/wacli/blob/main/internal/lock/lock.go) uses POSIX advisory locks via `syscall.Flock`:

```go
// 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:

```go
// 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`](https://github.com/steipete/wacli/blob/main/internal/lock/lock.go) cleans up the lock:

```go
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`](https://github.com/steipete/wacli/blob/main/internal/wa/client.go):

```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:

```go
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`](https://github.com/steipete/wacli/blob/main/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 `wacli` processes connect to WhatsApp using the same device identity keys stored in `session.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.
- `wacli` prevents concurrent access using a **file-based store lock** at `~/.wacli/LOCK` implemented in [`internal/lock/lock.go`](https://github.com/steipete/wacli/blob/main/internal/lock/lock.go).
- The `lock.Acquire()` function uses `syscall.Flock` with `LOCK_EX|LOCK_NB` to 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.