{"id":32357574,"url":"https://github.com/alexpota/jobguard","last_synced_at":"2026-01-20T17:30:22.521Z","repository":{"id":319467297,"uuid":"1067745684","full_name":"alexpota/jobguard","owner":"alexpota","description":"PostgreSQL durability for Redis-backed job queues (Bull, BullMQ, Bee-Queue)","archived":false,"fork":false,"pushed_at":"2025-10-18T12:26:45.000Z","size":1266,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-19T07:11:56.147Z","etag":null,"topics":["background-jobs","bee-queue","bull","bullmq","durability","fault-tolerance","job-queue","nodejs","postgresql","queue-persistence","redis","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alexpota.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-01T10:24:21.000Z","updated_at":"2025-10-18T12:39:50.000Z","dependencies_parsed_at":"2025-10-19T07:22:56.690Z","dependency_job_id":null,"html_url":"https://github.com/alexpota/jobguard","commit_stats":null,"previous_names":["alexpota/jobguard"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/alexpota/jobguard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexpota%2Fjobguard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexpota%2Fjobguard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexpota%2Fjobguard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexpota%2Fjobguard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alexpota","download_url":"https://codeload.github.com/alexpota/jobguard/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexpota%2Fjobguard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":280742567,"owners_count":26382923,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-24T02:00:06.418Z","response_time":73,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["background-jobs","bee-queue","bull","bullmq","durability","fault-tolerance","job-queue","nodejs","postgresql","queue-persistence","redis","typescript"],"created_at":"2025-10-24T12:10:15.282Z","updated_at":"2025-10-24T12:10:16.035Z","avatar_url":"https://github.com/alexpota.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# JobGuard\n\n[![npm](https://img.shields.io/npm/v/jobguard?logo=npm)](https://www.npmjs.com/package/jobguard)\n[![node](https://img.shields.io/node/v/jobguard)](https://nodejs.org/)\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue?logo=typescript)](https://www.typescriptlang.org/)\n[![CI](https://github.com/alexpota/jobguard/workflows/CI/badge.svg)](https://github.com/alexpota/jobguard/actions)\n[![coverage](https://img.shields.io/badge/coverage-85%25-brightgreen)](https://github.com/alexpota/jobguard)\n[![License](https://img.shields.io/npm/l/jobguard)](https://opensource.org/licenses/MIT)\n[![downloads](https://img.shields.io/npm/dm/jobguard)](https://www.npmjs.com/package/jobguard)\n\nPostgreSQL durability for Redis-backed job queues (Bull, BullMQ, Bee-Queue) with minimal integration.\n\n## Quick Start\n\n### Installation\n\n```bash\nnpm install jobguard pg\n```\n\n### Basic Usage\n\n```typescript\nimport Bull from 'bull';\nimport { JobGuard } from 'jobguard';\n\n// Create your queue as usual\nconst queue = new Bull('my-queue', 'redis://localhost:6379');\n\n// Add JobGuard for durability\nconst jobGuard = await JobGuard.create(queue, {\n  postgres: 'postgresql://localhost:5432/mydb',\n});\n\n// Use your queue normally - JobGuard works transparently\nawait queue.add('email', { to: 'user@example.com' });\n\n// Gracefully shutdown when done\nprocess.on('SIGTERM', async () =\u003e {\n  await jobGuard.shutdown();\n  await queue.close();\n});\n```\n\n## 🎬 Demo\n\n![JobGuard Stress Test](./assets/demo.gif)\n\n✅ **10,000 jobs • 60 workers • Redis crash at peak load • Zero jobs lost**\n\n[▶️ Run the interactive demo yourself →](./demo#readme)\n\n## Features\n\n- 🔒 **Drop-In Integration**: Wraps existing queues without modifying your queue code\n- 🔄 **Automatic Recovery**: Client-side reconciliation detects and recovers stuck jobs\n- 💓 **Heartbeat Support**: Long-running jobs signal liveness for accurate stuck detection\n- 📊 **Multi-Queue Support**: Works with Bull, BullMQ, and Bee-Queue\n- ⚡ **Low Overhead**: \u003c5ms per job operation, minimal memory footprint\n- 🛡️ **Fault Tolerant**: Circuit breaker pattern protects against PostgreSQL failures\n- 🎯 **Type Safe**: Full TypeScript support with strict typing\n\n## Table of Contents\n\n- [Quick Start](#quick-start)\n- [Demo](#-demo)\n- [Features](#features)\n- [Why JobGuard?](#why-jobguard)\n- [Database Setup](#database-setup)\n- [Configuration](#configuration)\n- [Advanced Usage](#advanced-usage)\n- [API Reference](#api-reference)\n- [Queue Library Support](#queue-library-support)\n- [How It Works](#how-it-works)\n- [Performance](#performance-considerations)\n- [Known Limitations](#known-limitations)\n- [Security](#security)\n- [Requirements](#requirements)\n- [FAQ](#faq)\n- [License](#license)\n- [Contributing](#contributing)\n\n## Why JobGuard?\n\nRedis-backed queues are fast but **volatile**. When Redis crashes or restarts, you lose:\n\n- ❌ Jobs currently being processed\n- ❌ Jobs waiting in the queue\n- ❌ Job history and audit trail\n- ❌ Ability to recover stuck jobs\n\n**JobGuard solves this** by adding PostgreSQL durability as a safety net, without changing your existing queue code.\n\n### The Problem: Speed vs Safety Trade-off\n\nMost teams face this dilemma:\n\n| Option | Result |\n|--------|--------|\n| Use Redis-only queues (Bull/BullMQ/Bee-Queue) | ⚡ Fast but lose jobs on crash |\n| Use PostgreSQL-only queues | 🛡️ Safe but sacrifice Redis speed |\n| Configure Redis AOF persistence | ⚠️ Still can lose data + complex setup |\n\n### The Solution: Best of Both Worlds\n\nJobGuard lets you keep Redis speed **and** get PostgreSQL safety:\n\n```typescript\n// Your existing queue\nconst queue = new Bull('my-queue', 'redis://localhost:6379');\n\n// Add JobGuard (just 3 lines)\nconst jobGuard = await JobGuard.create(queue, {\n  postgres: 'postgresql://localhost:5432/mydb',\n});\n\n// That's it! Your queue now has 100% durability\n```\n\n### Stress Test Results\n\n**Benchmark** (10,000 jobs, 60 workers, Redis crash at peak load):\n\n- 🎯 **Zero jobs lost** - 100% recovery after crash\n- 🛡️ **100% durability** - Every job persisted to PostgreSQL\n- ⏱️ **55 seconds** - Full stress test with crash recovery\n- 📊 **60 concurrent workers** - Proven scalability under load\n\n[▶️ Run the interactive stress test yourself](./demo#readme)\n\n## Database Setup\n\n**One-time setup:** Create the JobGuard table in your PostgreSQL database.\n\n### Option 1: Using psql (Recommended)\n\n```bash\npsql -d mydb -f node_modules/jobguard/schema/001_initial.sql\n```\n\n### Option 2: Programmatically\n\n```typescript\nimport { Pool } from 'pg';\nimport { readFileSync } from 'fs';\nimport { join } from 'path';\n\nconst pool = new Pool({ connectionString: 'postgresql://localhost:5432/mydb' });\nconst schema = readFileSync(\n  join(__dirname, 'node_modules/jobguard/schema/001_initial.sql'),\n  'utf8'\n);\nawait pool.query(schema);\n```\n\n### Option 3: Add to Your Existing Migrations\n\nCopy `node_modules/jobguard/schema/001_initial.sql` into your project's migration system (Knex, TypeORM, Prisma, etc.).\n\n## Configuration\n\n### Full Configuration Example\n\n```typescript\nconst jobGuard = await JobGuard.create(queue, {\n  // PostgreSQL connection (required)\n  postgres: {\n    host: 'localhost',\n    port: 5432,\n    database: 'mydb',\n    user: 'postgres',\n    password: 'secret',\n    max: 10, // Connection pool size\n    ssl: false,\n  },\n\n  // Or use connection string\n  // postgres: 'postgresql://localhost:5432/mydb',\n\n  // Reconciliation settings (optional)\n  reconciliation: {\n    enabled: true,\n    intervalMs: 30000, // Check every 30 seconds\n    stuckThresholdMs: 300000, // 5 minutes (minimum: 60000ms)\n    maxAttempts: 3,\n    batchSize: 100,\n    adaptiveScheduling: true, // Adjust interval based on load\n    rateLimitPerSecond: 20, // Max jobs to re-enqueue per second (default: 20)\n  },\n\n  // Logging settings (optional)\n  logging: {\n    enabled: true,\n    level: 'info', // 'debug' | 'info' | 'warn' | 'error'\n    prefix: '[JobGuard]',\n  },\n\n  // Persistence settings (optional)\n  persistence: {\n    retentionDays: 7, // Keep completed jobs for 7 days\n    cleanupEnabled: true,\n    cleanupIntervalMs: 3600000, // Cleanup every hour\n  },\n});\n```\n\n## Advanced Usage\n\n### Force Reconciliation\n\nTrigger immediate reconciliation:\n\n```typescript\nawait jobGuard.forceReconciliation();\n```\n\n### Get Queue Statistics\n\n```typescript\nconst stats = await jobGuard.getStats();\nconsole.log(`\n  Queue: ${stats.queueName}\n  Pending: ${stats.pending}\n  Processing: ${stats.processing}\n  Completed: ${stats.completed}\n  Failed: ${stats.failed}\n  Stuck: ${stats.stuck}\n  Total: ${stats.total}\n`);\n```\n\n### Multiple Queues\n\n```typescript\nconst emailQueue = new Bull('emails', redisUrl);\nconst emailGuard = await JobGuard.create(emailQueue, { postgres: postgresUrl });\n\nconst paymentQueue = new Bull('payments', redisUrl);\nconst paymentGuard = await JobGuard.create(paymentQueue, { postgres: postgresUrl });\n\n// Each queue is tracked independently\n```\n\n### Heartbeat for Long-Running Jobs\n\n**Problem**: For jobs with dynamic or long execution times (e.g., 20 seconds to 2 hours), a fixed `stuckThresholdMs` can cause false positives or slow recovery.\n\n**Solution**: Use heartbeats to signal that a job is still alive, regardless of how long it runs.\n\n```typescript\nimport { Worker } from 'bullmq';\nimport { JobGuard } from 'jobguard';\n\nconst queue = new Queue('data-sync', { connection: { host: 'localhost' } });\nconst jobGuard = await JobGuard.create(queue, {\n  postgres: postgresUrl,\n  reconciliation: {\n    stuckThresholdMs: 300000, // 5 minutes - short threshold works with heartbeats!\n  },\n});\n\n// Worker: Update heartbeat every 30 seconds during long-running jobs\nconst worker = new Worker('data-sync', async (job) =\u003e {\n  const heartbeatInterval = setInterval(async () =\u003e {\n    await jobGuard.updateHeartbeat(job.id!);\n  }, 30000); // Update every 30 seconds\n\n  try {\n    // Your long-running job logic\n    for (let i = 0; i \u003c largeDataset.length; i++) {\n      await processItem(largeDataset[i]);\n      // Heartbeat automatically updates in the background\n    }\n  } finally {\n    clearInterval(heartbeatInterval);\n  }\n}, { connection: { host: 'localhost' } });\n```\n\n**How it works**:\n- `updateHeartbeat(jobId)` updates the `last_heartbeat` timestamp in PostgreSQL\n- Stuck detection uses `COALESCE(last_heartbeat, updated_at)` - falls back to `updated_at` if no heartbeat\n- With regular heartbeats, jobs can run for hours without being marked stuck\n- If a worker crashes mid-heartbeat, the job is detected as stuck within `stuckThresholdMs` (fast recovery!)\n\n**Benefits**:\n- ✅ Fast recovery (5 minutes) for crashed jobs\n- ✅ No false positives for long-running jobs\n- ✅ Works with dynamic job durations (20 sec to 2 hours)\n- ✅ Backward compatible (jobs without heartbeats fall back to `updated_at`)\n\n## API Reference\n\n### `JobGuard.create(queue, config)`\n\nCreates and initializes a new JobGuard instance.\n\n**Parameters:**\n- `queue` **(required)** - Bull, BullMQ, or Bee-Queue instance\n- `config` **(required)** - Configuration object\n\n**Returns:** `Promise\u003cJobGuard\u003e`\n\n**Example:**\n```typescript\nconst jobGuard = await JobGuard.create(queue, {\n  postgres: 'postgresql://localhost:5432/mydb'\n});\n```\n\n### `jobGuard.getStats()`\n\nRetrieves current queue statistics from PostgreSQL.\n\n**Returns:** `Promise\u003cJobStats\u003e`\n\n**JobStats interface:**\n```typescript\n{\n  queueName: string;\n  pending: number;\n  processing: number;\n  completed: number;\n  failed: number;\n  stuck: number;\n  dead: number;\n  total: number;\n}\n```\n\n### `jobGuard.forceReconciliation()`\n\nManually triggers immediate reconciliation of stuck jobs.\n\n**Returns:** `Promise\u003cvoid\u003e`\n\n### `jobGuard.updateHeartbeat(jobId)`\n\nUpdates the heartbeat timestamp for a processing job to indicate it's still alive.\n\n**Parameters:**\n- `jobId` **(required)** - The job ID to update (string or number)\n\n**Returns:** `Promise\u003cvoid\u003e`\n\n**Example:**\n```typescript\n// In your worker process\nconst worker = new Worker('my-queue', async (job) =\u003e {\n  const heartbeat = setInterval(() =\u003e {\n    await jobGuard.updateHeartbeat(job.id);\n  }, 30000); // Every 30 seconds\n\n  try {\n    await longRunningTask(job.data);\n  } finally {\n    clearInterval(heartbeat);\n  }\n});\n```\n\n**Notes:**\n- Only updates heartbeat for jobs in `processing` status\n- Silently fails if job is not found or not processing (doesn't throw)\n- Recommended heartbeat interval: 30-60 seconds for most workloads\n\n### `jobGuard.shutdown()`\n\nGracefully shuts down JobGuard, stopping reconciliation and closing database connections.\n\n**Returns:** `Promise\u003cvoid\u003e`\n\n**Example:**\n```typescript\nprocess.on('SIGTERM', async () =\u003e {\n  await jobGuard.shutdown();\n  await queue.close();\n});\n```\n\n### Configuration Types\n\nFor full TypeScript type definitions and configuration options, see:\n- [Configuration Types](./src/types/config.ts)\n- [Job Types](./src/types/job.ts)\n\n## Queue Library Support\n\n### Bull\n\n```typescript\nimport Bull from 'bull';\nimport { JobGuard } from 'jobguard';\n\nconst queue = new Bull('my-queue', 'redis://localhost:6379');\nconst guard = await JobGuard.create(queue, { postgres: postgresUrl });\n```\n\n### BullMQ\n\n```typescript\nimport { Queue } from 'bullmq';\nimport { JobGuard } from 'jobguard';\n\nconst queue = new Queue('my-queue', { connection: { host: 'localhost' } });\nconst guard = await JobGuard.create(queue, { postgres: postgresUrl });\n```\n\n### Bee-Queue\n\n```typescript\nimport Queue from 'bee-queue';\nimport { JobGuard } from 'jobguard';\n\nconst queue = new Queue('my-queue', { redis: { host: 'localhost' } });\nconst guard = await JobGuard.create(queue, { postgres: postgresUrl });\n```\n\n## How It Works\n\nJobGuard provides durability through three mechanisms:\n\n1. **Job Tracking**: Intercepts job creation and tracks jobs in PostgreSQL\n2. **Event Monitoring**: Listens to queue events to update job status\n3. **Reconciliation**: Periodically checks for stuck jobs and re-enqueues them\n\n### Architecture\n\n![JobGuard Architecture](./assets/architecture.svg)\n\n**How it works:**\n1. **Queue Adapter** intercepts `queue.add()` and writes to both Redis (fast) and PostgreSQL (durable)\n2. **Event Monitor** listens to queue events and updates job status in PostgreSQL\n3. **Worker** (optional) sends heartbeats to PostgreSQL to signal long-running jobs are still alive\n4. **Reconciler** runs every 30 seconds to detect stuck jobs (using heartbeat or last update time) and re-enqueue them to Redis\n\n## Performance Considerations\n\n- **Overhead**: \u003c5ms per job operation\n- **Memory**: \u003c50MB for tracking 10,000 jobs\n- **Database**: Uses connection pooling (default: 10 connections)\n- **Reconciliation**: Adaptive scheduling reduces load during idle periods\n\n## Error Handling\n\nJobGuard uses a circuit breaker to prevent cascading failures:\n\n```typescript\nimport { CircuitBreakerOpenError } from 'jobguard';\n\ntry {\n  await jobGuard.getStats();\n} catch (error) {\n  if (error instanceof CircuitBreakerOpenError) {\n    console.error('PostgreSQL is unavailable, circuit breaker is open');\n  }\n}\n```\n\nWhen PostgreSQL is unavailable, JobGuard logs errors but allows your queue to continue operating normally. Jobs will be reconciled once PostgreSQL recovers.\n\n## Known Limitations\n\n### Race Condition Scenarios\n\nWhile JobGuard provides strong durability guarantees, some edge-case race conditions are **inherent to distributed systems** and cannot be completely eliminated:\n\n#### 1. Worker Crash During Job Processing\n\n**Scenario**: Worker processes a job successfully → crashes before sending completion event → reconciler re-enqueues the job\n\n**Impact**: Job may be processed twice\n\n**Mitigation**:\n- Implement idempotent job handlers in your application\n- Use database transactions or unique constraints for non-idempotent operations\n- Monitor duplicate processing via PostgreSQL job history\n\n#### 2. Bee-Queue Duplicate Jobs\n\n**Scenario**: Bee-Queue generates new job IDs when re-enqueueing stuck jobs (architectural limitation)\n\n**Impact**: Two job records exist in PostgreSQL (old marked 'failed', new marked 'pending')\n\n**Why this happens**: Unlike Bull/BullMQ, Bee-Queue doesn't support custom job IDs\n\n**Mitigation**:\n- The old job is marked as 'failed' to prevent conflict with partial index constraint\n- Only one job will be active in Redis at any time\n- Consider using Bull or BullMQ if this is a concern\n\n#### 3. Very Short-Lived Jobs\n\n**Scenario**: Job completes in \u003c100ms before event listeners attach\n\n**Impact**: Job may be marked as 'stuck' initially, then corrected\n\n**Mitigation**:\n- Use `stuckThresholdMs: 300000` (5 minutes) to avoid false positives\n- Very short jobs complete before reconciliation runs anyway\n\n### Configuration Constraints\n\n- **Minimum `stuckThresholdMs`**: 60,000ms (60 seconds) - prevents marking healthy jobs as stuck\n- **Rate limiting**: Reconciliation re-enqueues at 20 jobs/second by default (configurable via `rateLimitPerSecond`)\n- **Error message truncation**: Error messages are truncated to 5,000 characters and sanitized for security\n\n### Multi-Instance Reconciliation\n\n**⚠️ Not Supported**: Running multiple JobGuard instances with reconciliation enabled for the same queue can cause duplicate re-enqueue attempts.\n\n**Best Practice**: Only enable reconciliation (`reconciliation.enabled: true`) on **one** instance per queue:\n\n```typescript\n// Worker instances - reconciliation disabled\nconst jobGuard = await JobGuard.create(queue, {\n  postgres: postgresUrl,\n  reconciliation: { enabled: false },\n});\n\n// Single orchestrator instance - reconciliation enabled\nconst jobGuard = await JobGuard.create(queue, {\n  postgres: postgresUrl,\n  reconciliation: { enabled: true },\n});\n```\n\n### Performance Trade-offs\n\n- **PostgreSQL overhead**: Each job operation adds ~5ms latency\n- **Reconciliation impact**: Checking 10,000 stuck jobs takes ~2-5 seconds\n- **Memory usage**: ~50MB for tracking 10,000 jobs\n\n## Security\n\n### Reporting Vulnerabilities\n\n🔒 **Please do NOT open public issues for security vulnerabilities.**\n\nIf you discover a security issue, please **[Create a private security advisory](https://github.com/alexpota/jobguard/security/advisories/new)**\n\nWe will respond within 48 hours and work with you to address the issue.\n\n### Best Practices\n\n**Production Deployment:**\n- ✅ Use SSL/TLS for PostgreSQL connections (`ssl: true`)\n- ✅ Store connection strings in environment variables, not code\n- ✅ Use least-privilege database user with only required permissions:\n  ```sql\n  GRANT SELECT, INSERT, UPDATE, DELETE ON jobguard_jobs TO jobguard_user;\n  ```\n- ✅ Rotate database credentials regularly\n- ✅ Set appropriate `max_connections` for your PostgreSQL instance\n- ✅ Enable PostgreSQL audit logging for compliance requirements\n\n**What JobGuard Does NOT Do:**\n- ❌ JobGuard does not encrypt job data at rest (use PostgreSQL encryption)\n- ❌ JobGuard does not implement authentication (secure your PostgreSQL)\n- ❌ JobGuard does not sanitize job data (validate in your application)\n\n## Requirements\n\n- **Node.js**: 22.0+ (LTS)\n- **PostgreSQL**: 14+ (for B-tree deduplication)\n- **Queue Library**: Bull 4.12+, BullMQ 5.1+, or Bee-Queue 1.7+\n\n## FAQ\n\n### Why PostgreSQL only? Can I use MySQL/MongoDB?\n\n**No** - JobGuard currently requires PostgreSQL 14+.\n\nJobGuard uses PostgreSQL-specific features that are difficult to replicate in other databases:\n\n| Feature | Why It Matters | Other Databases |\n|---------|----------------|-----------------|\n| **JSONB** | Fast job data storage and queries without deserialization | MySQL JSON is slower; MongoDB has native JSON but lacks other features |\n| **Partial Indexes** | Only indexes active jobs - reduces storage and improves performance | MySQL has limited support; MongoDB supports but lacks transactional guarantees |\n| **ACID Transactions** | Guarantees zero data loss during writes | MongoDB added in 4.0 but still limited; MySQL supports but lacks JSONB |\n| **Advanced Indexes** | B-tree deduplication (PostgreSQL 14+) reduces index size by ~40% | Not available in MySQL/MongoDB |\n\n**Could other databases be supported?**\n\nSupporting MySQL or MongoDB would require:\n- Abstract database layer (adds complexity and maintenance burden)\n- Different schema implementations for each database\n- Performance compromises (MySQL's JSON is measurably slower than JSONB)\n- Extensive testing across multiple database versions\n\nThis significantly increases complexity for a feature that most users don't need. PostgreSQL is widely adopted in the Node.js ecosystem and provides the best combination of performance, reliability, and features for job durability.\n\n**What if my team uses MySQL/MongoDB?**\n\nYou have three options:\n\n1. **Add PostgreSQL for job tracking only** - JobGuard uses a single table with minimal overhead. Many teams run PostgreSQL alongside their primary database specifically for features like job durability.\n\n2. **Use PostgreSQL-only alternatives** - [Graphile Worker](https://github.com/graphile/worker) and [pg-boss](https://github.com/timgit/pg-boss) are PostgreSQL-native job queues (no Redis).\n\n3. **Request MySQL support** - If there's significant demand, MySQL support may be considered in the future. [Open an issue](https://github.com/alexpota/jobguard/issues) to discuss your use case.\n\n### Why not just use Redis persistence (RDB/AOF)?\n\nRedis persistence has limitations that JobGuard addresses:\n\n**Redis AOF with `appendfsync everysec` (recommended setting):**\n- Can lose up to 1 second of data on crash\n- Does not detect stuck jobs (worker crashes mid-processing)\n- Requires manual recovery after Redis restarts\n\n**Redis AOF with `appendfsync always` (100% durable):**\n- Significantly slower (every write waits for disk fsync)\n- Still doesn't detect stuck jobs\n- Still requires manual intervention for recovery\n\n**JobGuard provides:**\n- Zero data loss (PostgreSQL ACID guarantees)\n- Automatic stuck job detection and re-enqueueing\n- Full job history and audit trail\n- Minimal performance impact (~5ms overhead per job)\n\nYou can use Redis persistence AND JobGuard together for defense in depth, but JobGuard provides features that Redis persistence alone cannot.\n\n## License\n\nMIT\n\n## Contributing\n\nContributions are welcome! See [CONTRIBUTING.md](./.github/CONTRIBUTING.md) for development setup, testing, and code guidelines.\n\n---\n\n**Built by [Alex Potapenko](https://github.com/alexpota) • [Report Issues](https://github.com/alexpota/jobguard/issues)**\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexpota%2Fjobguard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexpota%2Fjobguard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexpota%2Fjobguard/lists"}