Introduction: Hitting the Queue Ceiling
If you've built anything beyond a trivial backend in Node.js, you've probably reached for BullMQ or Bee-Queue. They're fantastic tools—truly. I've used them for years. But there comes a point, usually under heavy load, where you start seeing the cracks. The latency spikes. The throughput plateaus. You throw more hardware at it, but the gains are marginal.
This isn't an academic problem. My day job involves maintaining the Valkey GLIDE client—the official, Rust-core client for Valkey and Redis. Staring at database internals all day gives you a unique perspective. You see exactly where the abstractions leak. You trace the round-trips. You feel the overhead of every unnecessary network hop. And that's why I got tired of the bottlenecks and built something else: Glide-MQ. It's not just another queue. It's a complete rethinking of how a job queue should interact with a data store like Valkey, and it's pushing 48,000 jobs per second. Let's break down why the old ways break and how a Rust-backed approach changes the game.
The Invisible Tax of the Standard Node.js Queue
Before we get to the solution, we need to diagnose the patient. The original Reddit post nailed the core issues, but let's expand on what "3+ round-trips per operation" actually means in practice. A simple job lifecycle—enqueue, process, acknowledge—isn't so simple.
Think about a typical flow in BullMQ. To add a job, the client might: 1) Check for Lua script cache (SHA), 2) EVAL the script if not cached, 3) Write the job data. That's already multiple commands. Processing involves blocking pops, moving items between lists, and updating state. Each of these is a separate command sent over the network, with its own latency cost. It adds up fast. At 10,000 jobs/sec, you're not just doing 10,000 operations—you're doing 30,000, 40,000, or more. The database and your network stack are doing most of the heavy lifting, not your business logic.
And that's the real tax. Your queue library isn't just a coordinator; it's a traffic generator. Every microsecond of Redis command parsing and network latency is multiplied. You end up scaling your database instances not because of data size, but because of command volume. It feels inefficient because it is.
Lua Scripts: A Double-Edged Sword
Most modern Redis queues rely heavily on Lua scripting to bundle operations atomically. It's the right idea—atomicity is critical. But the implementation has footguns. The big one? The NOSCRIPT error.
Here's the scenario: Your queue library loads a critical Lua script into Redis using SCRIPT LOAD and gets back a SHA1 hash. From then on, it calls EVALSHA with that hash for speed. But what happens if your Redis or Valkey instance restarts? The script cache is wiped. The next EVALSHA fails with NOSCRIPT. The library has to catch this, fall back to the slower EVAL with the full script source, and reload it. In a high-throughput system, this hiccup during a failover or upgrade can cause a cascade of errors and retries, killing your throughput right when you need resilience most.
Glide-MQ tackles this head-on. Instead of treating script caching as a hopeful optimization, it manages script lifecycles proactively. But more on that later. The other issue with scripts is they can become complex monoliths. A single script handling job admission, priority, delayed jobs, and rate-limiting is hard to debug and maintain. We need atomicity, but we also need modularity.
Beyond BRPOPLPUSH: The Primitive Problem
The original post mentions "legacy BRPOPLPUSH list primitives." This is a deep cut, but it's important. BRPOPLPUSH (or its newer sibling, BLMOVE) is the classic way to do reliable queueing in Redis: block until an item appears in a source list, then atomically pop and push it to a "processing" list. This ensures no job is lost if a worker crashes after picking it up.
The problem? It's a single operation. You can't easily incorporate priority, delayed jobs, or rate-limiting without breaking out of the atomic block or adding more round-trips. So, many libraries use it for the core pop, then layer other logic around it in non-atomic steps. This creates race conditions. Or, they abandon the simple primitive and move everything into a giant Lua script, bringing us back to the previous problem.
It's a trap. Simple primitives are limited. Complex scripts are fragile. We need a new model.
Enter Glide-MQ: Architecture from the Database Up
So, what did I build differently? Glide-MQ starts with a first-principles question: What does the database do well, and what should the client do well?
Valkey/Redis excels at fast, atomic in-memory operations and data structures. The client should excel at managing complexity, scheduling, and network efficiency. The bottleneck is often the chatty conversation between them. Glide-MQ, written in Rust and exposing a Node.js API via FFI (Foreign Function Interface), minimizes that conversation.
Instead of sending 3-5 commands per job operation, Glide-MQ's Rust engine batches logic intelligently. It uses a persistent connection pool managed in Rust, which is far more efficient than creating and tearing down connections in Node.js for high-volume traffic. It pre-loads and manages all necessary Lua scripts, handling NOSCRIPT errors transparently at the engine level before they ever hit your JavaScript code. The Rust core also handles serialization/deserialization, which for complex job data can be a surprising CPU hog in pure Node.js.
The result? The Node.js side sees a simple JavaScript API—similar to what you're used to—but every call is a direct bridge into the high-performance Rust engine. The chatty overhead is gone.
The 48k Jobs/Second Benchmark: Real-World Implications
"48,000 jobs per second" sounds like a vanity metric. Who really needs that? But it's not about the peak number; it's about what it represents: efficiency.
Hitting that number on a modest cloud instance (say, a 4-core machine) means the system is leaving massive headroom. Your latency stays low and predictable because the queue isn't saturating the database CPU with command parsing. You can handle the same workload with smaller, cheaper Valkey instances. Or, more importantly, you can handle sudden, spiky traffic without your job system becoming the bottleneck that brings everything down.
In my testing, a typical BullMQ setup might max out at 8k-12k jobs/sec on the same hardware before latency becomes erratic. The difference isn't just raw speed; it's the shape of the performance curve. Glide-MQ's curve is flatter, meaning performance degrades gracefully under load instead of falling off a cliff. This is the difference between getting paged at 3 AM and sleeping soundly.
Migrating Without Tears: A Practical Guide
Okay, you're convinced there's a better way. But rewriting your entire job processing system is a non-starter. The good news? You don't have to.
Glide-MQ's API is deliberately familiar. If you know BullMQ, you'll feel at home. The concepts of queues, jobs, workers, and events are the same. A basic producer looks almost identical:
// Instead of const Queue = require('bullmq');
const { Queue } = require('glide-mq');
const myQueue = new Queue('videoTranscode', { connection });
await myQueue.add('render', { videoId: 123, format: 'mp4' });
The real work is in your deployment. Since Glide-MQ has a native Rust component, you need to provide platform-specific binaries or build from source. For containerized environments, this means adding a build step to your Dockerfile or using multi-stage builds to compile the Rust core. It's an extra step, but tools like @node-rs/helper have made this process much smoother in the Node.js ecosystem.
Start with a non-critical, high-volume queue. Move your producer and consumer over and compare the metrics—CPU usage on your Valkey instance is often the most dramatic change. You don't need a big-bang migration.
Common Pitfalls and Questions from the Trenches
When I shared this on Reddit, some great questions came up. Let's address them head-on.
"Isn't this just moving the bottleneck to the FFI boundary?" A fair concern. The Node-to-Rust call does have a cost. But it's a fixed, tiny cost per batch of operations, not per job. The Rust engine can process hundreds of job lifecycle events internally before sending a single batched result back to Node.js. The FFI cost becomes negligible compared to eliminating 20 network round-trips.
"What about monitoring and tooling?" BullMQ has a great UI. Glide-MQ, being newer, is more bare-bones. It exposes standard metrics (queue length, latency histograms) via a Prometheus endpoint from the Rust side, which is actually more robust than a Node.js process that might be GC-paused. For a rich admin UI, you might need to build your own or use a generic Redis browser. This is the main trade-off: raw performance for mature ecosystem tools.
"Do I need to know Rust?" Not at all. The Rust layer is an internal implementation detail. You interact with it entirely via the Node.js API. It's a library, not a framework.
When to Stick with BullMQ (Seriously)
Glide-MQ isn't a magic bullet for every project. If you're processing 100 jobs a minute, the complexity of deploying a native addon isn't worth it. BullMQ is battle-tested, has fantastic documentation, and a rich ecosystem including the Bull Board UI.
Stick with BullMQ if: your throughput is low-to-moderate, you heavily rely on the admin UI, your team is uncomfortable with native binaries, or you're using advanced features like parent/child job relationships that are still maturing in newer alternatives. The best tool is the one that gets the job done with the least operational headache for your team.
But if you're staring at your Grafana dashboards watching Redis CPU climb linearly with job volume, if you're adding Valkey shards just to handle queue chatter, or if latency spikes are causing user-facing issues—then it's time to look at the architecture of your queue itself. The bottleneck might not be your code, but the conversation around it.
Conclusion: The Future is Hybrid
The story of Glide-MQ isn't really about Rust versus JavaScript. It's about using the right tool for each layer of the stack. Node.js is phenomenal for I/O-bound business logic, rapid development, and ecosystem glue. Rust is phenomenal for CPU-bound, performance-critical systems programming. Combining them via FFI lets you capture the strengths of both.
As backend systems push beyond 100k requests per second, these hybrid architectures are becoming essential. The pure Node.js queue has served us well, but the ceiling is real. The next generation of high-performance backend tools won't be written in a single language. They'll be built by specialists, layer by layer.
If you're hitting that ceiling, the path forward isn't just more servers. It's a more intelligent conversation with your data. And sometimes, that means letting a Rust engine do the talking.
You can find the Glide-MQ project on GitHub. Drop an issue if you try it and hit a snag—or if you break 50k jobs/sec. I'd love to hear about it.