Distributed worker queues backed by S3 with zero-duplication guarantees.
Navigation: β Plugin Index | Guides β | FAQ β
Complete documentation organized by topic. Start here to find what you need.
- β‘ TLDR - 30-second overview
- π Quick Start - Get running in minutes
- π¦ Dependencies - What you need
| Guide | Focus |
|---|---|
| onMessage Handler | Writing message processing logic |
| Configuration | Plugin options & real-world setups |
| Architecture | Internal design & event system |
| Performance & Scalability | Optimization & multi-pod deployment |
| Patterns & Best Practices | FAQ, troubleshooting, patterns |
- Quick questions? Check FAQ
- Message processing? See onMessage Handler Guide
- Configuration help? See Configuration Guide
- Production scaling? See Performance & Scalability Guide
- Troubleshooting? See Patterns Guide
Distributed queue system using S3 as backend, with zero duplication guarantee.
3 lines to get started:
const queue = new S3QueuePlugin({ resource: 'tasks', onMessage: async (task) => { console.log('Processing:', task); } });
await db.usePlugin(queue);
await tasks.enqueue({ type: 'send-email', data: {...} });π§© Namespaces: Provide
namespace: 'emails'(or pass an alias viadb.usePlugin) to run multiple S3QueuePlugin instances. Queue/dead-letter resources will be emitted asplg_emails_β¦.
Key features:
- β Zero duplication (distributed locks + ETag + cache)
- β Visibility timeout (like AWS SQS)
- β Automatic retry with exponential backoff
- β Dead letter queue
- β Configurable worker pool
- β Custom metadata & partitions (per-client queues, count by partition)
When to use:
- π§ Email/SMS queues
- π¬ Media processing
- π Report generation
- π Background jobs
- π Webhook delivery
Required:
pnpm install s3db.jsNO Peer Dependencies!
S3QueuePlugin is built into s3db.js core with zero external dependencies!
Why Zero Dependencies?
- β Pure JavaScript implementation (no external libraries)
- β Works instantly after installing s3db.js
- β No version conflicts or compatibility issues
- β Lightweight and fast (~20KB plugin code)
- β Perfect for serverless (AWS Lambda, Cloudflare Workers, Vercel)
What's Included:
- Queue Management: Enqueue, dequeue, visibility timeout, message lifecycle
- Worker Pool: Configurable concurrent worker threads with graceful shutdown
- Distributed Locks: ETag-based pessimistic locking for zero-duplication guarantee
- Dead Letter Queue: Automatic failed message handling with retry logic
- Exponential Backoff: Intelligent retry delays (2s β 4s β 8s β 16s β 32s)
- Event System: Leverages s3db.js resource events for monitoring
- Cache Integration: Uses CachePlugin for deduplication tracking (optional)
Architecture:
S3QueuePlugin uses s3db.js core primitives:
- Resources: Queue and dead-letter resources auto-created
- Metadata: Message status, retry count, visibility timeout stored in S3 metadata
- Partitions: Status-based partitions for O(1) pending message lookup
- TTL: Optional TTLPlugin integration for auto-cleanup of processed messages
- Locks: PluginStorage with ETag validation for distributed locking
Minimum Node.js Version: 18.x (for async/await, worker threads, native performance)
Platform Support:
- β Node.js 18+ (server-side, recommended)
- β AWS Lambda (serverless functions)
- β Cloudflare Workers (edge computing)
- β Vercel Edge Functions
β οΈ Browser (limited - no worker pool, single-threaded polling only)
Production Recommendations:
- Use TTLPlugin for automatic cleanup of processed messages (prevent S3 bloat)
- Configure worker pool size based on your workload (default: 3 workers)
- Set visibility timeout appropriate for your task duration (default: 30s)
- Enable cache for deduplication tracking (CachePlugin recommended)
- Monitor events for queue health (
plg:queue:stats,plg:queue:error)
// Production-ready configuration
import { Database } from 's3db.js';
import { S3QueuePlugin, CachePlugin, TTLPlugin } from 's3db.js';
const db = new Database({ connectionString: 's3://key:secret@bucket' });
// Add cache for deduplication
await db.usePlugin(new CachePlugin({ driver: 'memory', ttl: 3600000 }));
// Add TTL for auto-cleanup (processed messages deleted after 7 days)
await db.usePlugin(new TTLPlugin({ defaultTTL: 604800000 }));
// Create queue
const queue = new S3QueuePlugin({
resource: 'tasks',
workers: 5, // 5 concurrent workers
visibilityTimeout: 300, // 5 minutes per task
maxRetries: 3, // Retry 3 times before DLQ
onMessage: async (task) => {
// Process task
console.log('Processing:', task);
}
});
await db.usePlugin(queue);
await db.connect();In multi-pod/multi-instance deployments, we need exactly one instance to publish dispatch tickets to avoid:
- β Duplicate ticket publishing
- β Race conditions in FIFO/LIFO ordering
- β Wasted resources from redundant coordination work
Coordinator Mode solves this by automatically electing one instance as the "coordinator" responsible for publishing tickets. All other instances remain workers that process messages.
- β Automatic Election: No manual configuration, works out-of-the-box
- β Fault Tolerance: If coordinator dies, new one is elected automatically
- β Zero Duplication: Only coordinator publishes tickets
- β Scalable: Add/remove instances without breaking coordination
- β Battle-Tested: Uses epoch-based leadership with cold start protection
// Multi-instance deployment - NO changes needed!
// Instance 1
const queueA = new S3QueuePlugin({
resource: 'tasks',
enableCoordinator: true, // Enabled by default
onMessage: async (task) => { /* process */ }
});
// Instance 2 (same config)
const queueB = new S3QueuePlugin({
resource: 'tasks',
enableCoordinator: true,
onMessage: async (task) => { /* process */ }
});
// Result: Only ONE instance publishes tickets, both process messages| Option | Type | Default | Description |
|---|---|---|---|
enableCoordinator |
boolean | true |
Enable coordinator mode |
heartbeatInterval |
number | 10000 |
Heartbeat frequency (ms) |
dispatchInterval |
number | 100 |
How often coordinator publishes tickets (ms) |
ticketBatchSize |
number | 10 |
Messages per ticket batch |
coldStartDuration |
number | 0 |
Cold-start observation duration (ms) |
startupJitterMin |
number | 0 |
Minimum startup jitter (ms) |
startupJitterMax |
number | 5000 |
Maximum startup jitter (ms) |
skipColdStart |
boolean | false |
Skip cold start (testing only!) |
queue.on('plg:s3-queue:coordinator-elected', ({ workerId, epoch }) => {
console.log(`New coordinator: ${workerId}`);
});
queue.on('plg:s3-queue:coordinator-promoted', ({ workerId }) => {
console.log(`This worker is now coordinator`);
});
queue.on('plg:s3-queue:tickets-published', ({ count, coordinatorId }) => {
console.log(`Coordinator published ${count} tickets`);
});π Full Coordinator Documentation β
Comprehensive guide covering:
- Election algorithm (lexicographic ordering)
- Epoch system (guaranteed leadership terms)
- Cold start phases (prevents race conditions)
- Troubleshooting multi-instance issues
- Implementation details for plugin developers
For multiple workers starting together on the same queue, keep these in sync:
- same
resourceand same environment enableCoordinator: true(default)startupJitterMin+startupJitterMaxto stagger election startupmaxPollInterval>pollIntervalso idle workers back off
Example:
new S3QueuePlugin({
resource: 'tasks',
enableCoordinator: true,
startupJitterMin: 500,
startupJitterMax: 2500,
pollInterval: 1000,
maxPollInterval: 12000
});Unlike traditional queues that guarantee "at-least-once" delivery, S3Queue achieves exactly-once processing through a combination of:
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Zero Duplication Architecture β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Layer 1: PluginStorage Locks β
β β Prevents concurrent cache checks β
β β
β Layer 2: Deduplication Cache (Distributed) β
β β PluginStorage + local TTL cache β
β β
β Layer 3: ETag Atomicity (S3 Native) β
β β Atomic claim via conditional update β
β β
β Result: 0% Duplication Rate π β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
See Architecture Guide for complete details.
Each message gets a distributed lock during claim. See Architecture Guide for the complete flow.
Long-running handlers sometimes crash before completing. S3Queue continuously scans for stuck messages and automatically requeues them. See Configuration Guide for tuning options.
Workers gradually back off when the queue is empty, reducing S3 requests. See Performance Guide for optimization details.
Just like AWS SQS - messages become invisible while being processed. See onMessage Handler Guide for usage patterns.
Attempt 1: Fail βββΊ Wait 1 second βββΊ Retry
Attempt 2: Fail βββΊ Wait 2 seconds βββΊ Retry
Attempt 3: Fail βββΊ Wait 4 seconds βββΊ Retry
Attempt 4: Fail βββΊ Move to Dead Letter Queue β οΈSee Configuration Guide for retry configuration.
npm install s3db
# or
pnpm add s3dbimport { Database, S3QueuePlugin } from 's3db';
// 1. Connect to S3
const db = new Database({
connection: 's3://KEY:SECRET@localhost:9000/my-bucket'
});
await db.connect();
// 2. Create resource
const tasks = await db.createResource({
name: 'tasks',
attributes: {
id: 'string|required',
type: 'string|required',
data: 'json'
}
});
// 3. Setup queue
const queue = new S3QueuePlugin({
resource: 'tasks',
onMessage: async (task) => {
console.log('Processing:', task.type);
// Your logic here
return { done: true };
}
});
await db.usePlugin(queue);
// 4. Enqueue tasks
await tasks.enqueue({
type: 'send-email',
data: { to: 'user@example.com' }
});
// That's it! Workers are already processing π- Write your first handler: onMessage Handler Guide
- Configure for production: Configuration Guide
- Learn the architecture: Architecture Guide
- Scale to multiple pods: Performance & Scalability Guide
- Troubleshoot issues: Patterns & Best Practices Guide
To reset only pending data, use truncateQueue().
const result = await tasks.truncateQueue({
includeDeadLetter: true
});
console.log(result);
// { queueDeleted: 10, deadLetterDeleted: 2 }To fully delete queue resources (and optionally dead-letter data), use deleteQueue().
const result = await tasks.deleteQueue();
console.log(result);
// { queueDeleted: 10, deadLetterDeleted: 2, removedTickets: 5, queueResourceDeleted: true, deadLetterResourceDeleted: true }The deleteQueue method clears runtime state and removes internal queue resources so the queue starts from a clean baseline.
- Coordinator Mode Documentation - Multi-pod coordination
- Configuration Guide - All plugin options
- Architecture Guide - How it works internally
- FAQ - Common questions & answers