# Troubleshooting bunqueue: Common Issues & Fixes

Fix common bunqueue problems: SQLite database locks, memory leaks, connection timeouts, job processing failures, and embedded mode issues.

Canonical: https://bunqueue.dev/troubleshooting/

---

import { Tabs, TabItem } from '@astrojs/starlight/components';

<div class="bq-wrap bq-hero">
  <span class="bq-eyebrow">reference · troubleshooting</span>
  <h1 class="bq-hero-h1 bq-bench-h1">Troubleshooting, cause and <em>fix.</em></h1>
  <p class="bq-hero-sub">Symptoms, causes and fixes for the issues people actually hit: SQLite locks, embedded mode misconfiguration, stuck jobs, half-open connections and backup failures.</p>
</div>

## Installation Issues

### "bunqueue is Bun-only and requires the Bun runtime"

Running under Node.js throws:

> bunqueue is Bun-only and requires the Bun runtime (https://bun.sh). Node.js is not supported: install Bun and run your program with `bun`.

bunqueue only works with Bun (v1.4.0+), not Node.js, run your program with `bun`, not `node`.

```bash
# Check if Bun is installed
bun --version

# Install Bun if needed
curl -fsSL https://bun.sh/install | bash
```

### Permission errors on install

```bash
# Try with sudo (not recommended)
sudo bun add bunqueue

# Better: fix npm permissions
mkdir ~/.bun
chown -R $(whoami) ~/.bun
```

## Database Issues

### "SQLITE_BUSY: database is locked"

Multiple processes trying to write simultaneously.

**Solutions:**
1. Use WAL mode (default in bunqueue)
2. Ensure only one server instance per database file
3. Use server mode for multi-process access

```bash
# Check for multiple processes
lsof ./data/queue.db

# Kill stale processes
pkill -f bunqueue
```

### "SQLITE_CORRUPT: database disk image is malformed"

Database corruption, usually from crash during write.

**Solutions:**
1. Restore from S3 backup
2. Delete and recreate database (data loss)

```bash
# Restore from backup
bunqueue backup list
bunqueue backup restore <key> --force

# Or recreate (loses data)
rm ./data/queue.db*
bunqueue start
```

### Database file keeps growing

SQLite doesn't automatically reclaim space.

```bash
# Vacuum the database (run when server is stopped)
sqlite3 ./data/queue.db "VACUUM;"

# Enable auto-vacuum (before creating database)
sqlite3 ./data/queue.db "PRAGMA auto_vacuum = INCREMENTAL;"
```

## Embedded Mode Issues

### "Command timeout" error

```
error: Command timeout
      queue: "my-queue",
      context: "pull"
```

This error means your Worker is trying to connect to a TCP server instead of using embedded mode.

**Solution:** Add `embedded: true` to **both** Queue and Worker:

```typescript
// WRONG - Worker defaults to TCP mode
const queue = new Queue('tasks', { embedded: true });
const worker = new Worker('tasks', processor); // Missing embedded: true!

// CORRECT - Both have embedded: true
const queue = new Queue('tasks', { embedded: true });
const worker = new Worker('tasks', processor, { embedded: true });
```

### SQLite database not created

The database is only created when a data path is configured.

**Solution (embedded mode):**

```typescript
import { Queue, QueueEvents, Worker, shutdownManager } from 'bunqueue/client';

// Pass dataPath directly
const queue = new Queue('tasks', { embedded: true, dataPath: './data/bunqueue.db' });
const worker = new Worker('tasks', processor, { embedded: true, dataPath: './data/bunqueue.db' });
```

**Solution (server mode):** Use a [configuration file](/guide/configuration/) or set `BUNQUEUE_DATA_PATH`:

```bash
BUNQUEUE_DATA_PATH=./data/bunqueue.db bunqueue start
```

:::note
Without `dataPath` or `BUNQUEUE_DATA_PATH`, bunqueue runs in-memory (no persistence across restarts).
:::

### Jobs not persisted across restarts

In embedded mode the shared QueueManager is a process-wide singleton initialized by the **first** embedded `Queue`, `Worker`, or `QueueEvents`. If that client has no explicit or environment data path, the manager starts in-memory. Clients that omit `dataPath` then join the active manager.

A later explicit path is never ignored. If it identifies a different database, construction throws an `Embedded QueueManager dataPath conflict` error before any job can be accepted with the wrong durability. Relative, absolute, and symlink spellings of the same existing database are accepted.

Common pitfall: setting `process.env.DATA_PATH` at the top of `main.ts` and then importing a module that constructs a Queue, Worker, or QueueEvents. ESM imports are hoisted, so the module and its constructors run **before** the assignment.

**Solution 1 (recommended):** pass `dataPath` directly in the constructor options, no env var needed:

```typescript
const queue = new Queue('tasks', { embedded: true, dataPath: './data/bunqueue.db' });
const worker = new Worker('tasks', processor, { embedded: true, dataPath: './data/bunqueue.db' });
const events = new QueueEvents('tasks', { embedded: true, dataPath: './data/bunqueue.db' });
```

**Solution 2:** set the env var before the process starts:

```bash
BUNQUEUE_DATA_PATH=./data/bunqueue.db bun run main.ts
```

To switch databases in one process, close every embedded client and call
`shutdownManager()` before constructing the next one. Use separate processes
or TCP brokers when databases must remain active concurrently.

## Job Processing Issues

### Jobs stuck in "active" state

Worker crashed while processing.

**Solutions:**
1. Enable stall detection
2. Restart workers

```typescript
queue.setStallConfig({
  enabled: true,
  stallInterval: 30000,
  maxStalls: 3,
});
```

### Jobs not being processed

**Check these:**
1. Is the queue paused?
2. Is there a worker for this queue?
3. Is rate limiting blocking jobs?

<Tabs syncKey="lang">
<TabItem label="Bun">

```typescript
// Check if paused
const isPaused = await queue.isPausedAsync();

// Check counts
const counts = await queue.getJobCountsAsync();
console.log(counts);
```

</TabItem>
<TabItem label="Node.js / Deno">

```typescript
// Check if paused
const isPaused = await queue.isPaused();

// Check counts
const counts = await queue.getJobCounts();
console.log(counts);
```

</TabItem>
<TabItem label="Python">

```python
# Check if paused
is_paused = queue.is_paused()

# Check counts
counts = queue.get_job_counts()
print(counts)
```

</TabItem>
<TabItem label="PHP">

```php
// Check if paused
$isPaused = $queue->isPaused();

// Check counts
$counts = $queue->getJobCounts();
var_dump($counts);
```

</TabItem>
<TabItem label="Go">

```go
// Check if paused
paused, _ := queue.IsPaused()

// Check counts
counts, _ := queue.GetJobCounts()
fmt.Println(paused, counts)
```

</TabItem>
<TabItem label="Rust">

```rust
// Check if paused
let paused = queue.is_paused()?;

// Check counts
let counts = queue.get_job_counts()?;
println!("{paused} {counts:?}");
```

</TabItem>
<TabItem label="Elixir">

```elixir
# Check if paused
{:ok, paused} = Bunqueue.Queue.is_paused(queue)

# Check counts
{:ok, counts} = Bunqueue.Queue.get_job_counts(queue)
```

</TabItem>
</Tabs>

### "Job is not active" when calling updateProgress

Progress can only be updated while the job is **active** (being processed). The server rejects `Progress` for any other state with `Job is not active (current state: ...)`. Typical causes:

- Calling `job.updateProgress()` after the processor returned (job already completed)
- The job was failed/stalled/cancelled underneath a long-running processor
- Updating progress from outside the worker while the job is still waiting

Treat it as a signal that you no longer own the job, not as a transient error.

### getJobs() does not show a job I just added

In SQLite mode, job listings (`getJobs`, `GetJobs` over TCP) read from SQLite,
while non-durable pushes go through a write buffer that flushes about every
10ms. A job added a moment ago can therefore be missing from a listing for up
to ~10ms. Use `getJob(id)` / `getState(id)` (which read the in-memory index) for
read-after-write checks, or add the job with `durable: true` to bypass the
buffer.

PostgreSQL admissions are transactional and do not use the SQLite write
buffer. The broker that accepts a push refreshes its local PostgreSQL projection
before acknowledging it. Other brokers converge through the durable outbox and
`LISTEN` wakeups, with polling as the fallback, so a listing sent immediately to
a different broker can briefly reflect its previous projection.

### Jobs failing immediately

Check the error in failed event:

<Tabs syncKey="lang">
<TabItem label="Bun">

```typescript
worker.on('failed', (job, error) => {
  console.error('Job failed:', error);
  console.error('Job data:', job.data);
  console.error('Attempts:', job.attemptsMade);
});
```

</TabItem>
<TabItem label="Node.js / Deno">

```typescript
worker.on('failed', (job, error) => {
  console.error('Job failed:', error);
  console.error('Job data:', job.data);
  console.error('Attempts:', job.attempts);
});
```

</TabItem>
<TabItem label="Python">

```python
def on_failed(job, err):
    print("Job failed:", err)
    print("Job data:", job.data)
    print("Attempts:", job.attempts)

worker.on("failed", on_failed)
```

</TabItem>
<TabItem label="PHP">

```php
$worker->on('failed', function ($job, $error) {
    error_log('Job failed: ' . $error->getMessage());
    error_log('Attempts: ' . $job->attemptsMade());
});
```

</TabItem>
<TabItem label="Go">

```go
worker.On("failed", func(args ...any) {
    job := args[0].(*bunqueue.Job)
    err := args[1].(error)
    log.Printf("job %s failed after %d attempts: %v", job.ID(), job.AttemptsMade(), err)
})
```

</TabItem>
<TabItem label="Rust">

```rust
// Rust has no worker event emitter: a failed attempt surfaces where you return it,
// and transport-level problems arrive on the connection telemetry callback.
let worker = Worker::new(
    "queue",
    |job| {
        process(job.data()).map_err(|e| {
            eprintln!("job {} failed on attempt {}: {e}", job.id(), job.attempts_made());
            ProcessError::retryable(e.to_string())
        })
    },
    WorkerOptions::default(),
);
```

</TabItem>
<TabItem label="Elixir">

```elixir
# Elixir has no worker event emitter: a failed attempt surfaces where you return it,
# and transport-level problems arrive on the connection `:event_handler` callback.
worker =
  Bunqueue.Worker.new("queue", fn job ->
    case process(job.data) do
      {:ok, result} ->
        {:ok, result}

      {:error, reason} ->
        IO.puts("job #{job.id} failed on attempt #{job.attempts_made}: #{inspect(reason)}")
        {:error, reason}
    end
  end)
```

</TabItem>
</Tabs>

### Memory usage keeps growing

**Possible causes:**
1. Jobs not being removed after completion
2. Too many jobs in DLQ
3. Memory leak in processor

```typescript
// Enable removeOnComplete
await queue.add('task', data, {
  removeOnComplete: true,
});

// Purge old DLQ entries
queue.purgeDlq();

// Check for leaks in processor
worker.on('completed', () => {
  console.log('Memory:', process.memoryUsage().heapUsed);
});
```

## Connection Issues

### "Connection refused" to server

Server not running or wrong port.

```bash
# Check if server is running
ps aux | grep bunqueue

# Check listening ports
lsof -i :6789
lsof -i :6790

# Start server
bunqueue start
```

### TCP connection drops

Network issues or server overload.

```typescript
// Add reconnection logic
let client = createClient();

client.on('error', async () => {
  await sleep(1000);
  client = createClient();
});
```

### Worker stalls on a half-open connection (throughput drops to 0)

A worker's TCP socket can go **half-open**, the peer vanishes with no FIN/RST (host
suspended/hibernated, NAT or load-balancer silently dropping an idle connection). Writes
still succeed and no `close` event fires, so the symptom is: every command rejects with
`Command timeout`, `consecutiveErrors` climbs, jobs pile up in `waiting` with `active=0`,
and throughput sits at 0.

bunqueue detects this and reconnects automatically via two signals: the health-check ping
(`maxPingFailures`) **and** consecutive command timeouts (`maxCommandTimeouts`, default 3).
With default timings (`pingInterval`/`commandTimeout` = 30s) recovery takes up to ~120s.
For faster recovery, tighten the detection cadence:

```typescript
const worker = new Worker('q', handler, {
  connection: {
    host, port,
    pingInterval: 10_000,    // health-check every 10s (0 disables)
    commandTimeout: 5_000,   // fail a command after 5s
    maxCommandTimeouts: 3,   // 3 consecutive timeouts → reconnect (0 disables)
  },
});
```

This recovers in ~tens of seconds and works even with the ping disabled. If a *fresh*
connection also can't be established (e.g. the server is genuinely unreachable, a firewall
dropping inbound SYNs, not just an idle drop), no client can reconnect until connectivity
returns; auto-reconnect with infinite attempts resumes on its own once it does.

### Authentication failures

```bash
# Check token is set
echo $AUTH_TOKENS

# Test with curl
curl -H "Authorization: Bearer your-token" \
  http://localhost:6790/health
```

## Performance Issues

### Slow job processing

**Optimize:**
1. Increase worker concurrency
2. Use batch operations
3. Check database I/O

<Tabs syncKey="lang">
<TabItem label="Bun">

```typescript
// Increase concurrency
const worker = new Worker('queue', processor, {
  concurrency: 20,
});

// Use bulk add
await queue.addBulk([...jobs]);

// Workers batch pulls and acks automatically; tune the batch size
const batchWorker = new Worker('queue', processor, { batchSize: 100 });
```

</TabItem>
<TabItem label="Node.js / Deno">

```typescript
// Increase concurrency
const worker = new Worker('queue', processor, {
  concurrency: 20,
});

// Use bulk add
await queue.addBulk([...jobs]);

// Workers batch pulls automatically; tune the batch size
const batchWorker = new Worker('queue', processor, { batchSize: 100 });
```

</TabItem>
<TabItem label="Python">

```python
# Increase concurrency
worker = Worker("queue", process, concurrency=20)

# Use bulk add
queue.add_bulk(jobs)

# Workers batch pulls automatically; tune the batch size
batch_worker = Worker("queue", process, batch_size=100)
```

</TabItem>
<TabItem label="PHP">

```php
// The PHP worker is sequential by design; scale by running more worker processes

// Use bulk add
$queue->addBulk($jobs);

// Workers batch pulls automatically; tune the batch size
$worker = new Worker('queue', $processor, ['batchSize' => 100]);
```

</TabItem>
<TabItem label="Go">

```go
// Increase concurrency
worker := bunqueue.NewWorker("queue", processor, bunqueue.WorkerOptions{Concurrency: 20})

// Use bulk add
ids, err := queue.AddBulk(entries)

// Workers batch pulls automatically; tune the batch size
batchWorker := bunqueue.NewWorker("queue", processor, bunqueue.WorkerOptions{BatchSize: 100})
```

</TabItem>
<TabItem label="Rust">

```rust
use bunqueue_client::{Worker, WorkerOptions};

// Increase concurrency
let worker = Worker::new("queue", processor, WorkerOptions {
    concurrency: 20,
    ..Default::default()
});

// Use bulk add
let ids = queue.add_bulk(entries)?;

// Workers batch pulls automatically; tune the batch size
let batch_worker = Worker::new("queue", processor, WorkerOptions {
    batch_size: 100,
    ..Default::default()
});
```

</TabItem>
<TabItem label="Elixir">

```elixir
# Increase concurrency
worker = Bunqueue.Worker.new("queue", processor, concurrency: 20)

# Use bulk add
{:ok, ids} = Bunqueue.Queue.add_bulk(queue, jobs)

# Workers batch pulls automatically; tune the batch size
worker = Bunqueue.Worker.new("queue", processor, batch_size: 100)
```

</TabItem>
</Tabs>

### High latency on pull

**Check:**
1. Index on queue table
2. Too many delayed jobs
3. Database on slow disk

```sql
-- Check indexes exist
.indices jobs

-- Check delayed jobs count
SELECT COUNT(*) FROM jobs WHERE state = 'delayed';
```

### Server CPU at 100%

Too many connections or jobs.

```bash
# Check connection count
bunqueue stats

# Reduce polling frequency
# Add backoff in clients
```

## Backup Issues

### S3 backup failing

```bash
# Check credentials
echo $S3_ACCESS_KEY_ID
echo $S3_BUCKET

# Test connectivity
aws s3 ls s3://$S3_BUCKET/

# Check logs
bunqueue backup status
```

### Restore failing

```bash
# List available backups
bunqueue backup list

# Force restore (overwrites existing)
bunqueue backup restore <key> --force
```

## Sandboxed Worker Issues

:::danger[SandboxedWorker is experimental]
`SandboxedWorker` depends on [Bun Workers](https://bun.sh/docs/runtime/workers), which are **experimental**. Known issues include memory growth, thread duplication, and segfaults across Bun versions.

**For production, use the standard `Worker` instead**, it provides the same API (events, concurrency, heartbeats, retries) without any experimental dependencies. See [Worker vs SandboxedWorker](/guide/worker/sandboxed/#worker-vs-sandboxedworker).
:::

### Segmentation fault when terminating workers

If you experience crashes (segfaults) when using `SandboxedWorker`, especially during worker timeout or error handling, this is a **known Bun bug**.

**Symptoms:**
- `Segmentation fault at address 0xE8`
- `Worker has been terminated` errors
- Crashes during `worker.terminate()` calls
- Unexpected memory growth or thread duplication ([#52](https://github.com/egeominotti/bunqueue/issues/52))

**Solution:** Switch to the standard `Worker` for production workloads:

```typescript
// ❌ SandboxedWorker: experimental, may crash
const worker = new SandboxedWorker('queue', {
  processor: './processor.ts',
  concurrency: 4,
});

// ✅ Worker: stable, production-ready, same functionality
const worker = new Worker('queue', async (job) => {
  // same logic from your processor.ts
  return result;
}, { embedded: true, concurrency: 4 });
```

**If you must use SandboxedWorker:**
- Pin your Bun version, behavior varies across releases
- Use graceful shutdown (`await worker.stop()`) instead of force termination
- Use longer timeout values to avoid frequent terminations
- Monitor memory usage closely

These issues will be resolved when Bun stabilizes their Worker API.

## Common Error Messages

| Error | Cause | Solution |
|-------|-------|----------|
| `Command timeout` | Worker missing `embedded: true` | Add `embedded: true` to Worker options |
| `SQLITE_BUSY` | Database locked | Use single writer |
| `SQLITE_FULL` | Disk full | Free disk space |
| `ECONNREFUSED` | Server not running | Start server or use embedded mode |
| `ETIMEDOUT` | Network issue | Check connectivity |
| `Job not found` | Already completed/removed | Check job lifecycle |
| `Segmentation fault` | Bun Worker termination bug | Use graceful shutdown, see above |

## Debug Mode

The server logs to stdout. Switch to structured JSON logs for easier filtering:

```bash
# Server mode (structured logs)
LOG_FORMAT=json bunqueue start

# Pipe to a file if you want persistent logs
LOG_FORMAT=json bunqueue start >> /var/log/bunqueue.log 2>&1
```

## Getting Help

If these solutions don't help:

1. Check [GitHub Issues](https://github.com/egeominotti/bunqueue/issues)
2. Search [Discussions](https://github.com/egeominotti/bunqueue/discussions)
3. Open a new issue with:
   - bunqueue version
   - Bun version
   - OS and hardware
   - Error message and stack trace
   - Minimal reproduction code

:::tip[Related Guides]
- [Monitoring & Prometheus Metrics](/guide/monitoring/) - Set up monitoring to prevent issues
- [FAQ](/faq/) - Frequently asked questions
- [Stall Detection & Recovery](/guide/stall-detection/) - Debug stalled jobs
:::