Back to Blog

BullMQ Dashboard (Bull Board) Not Accessible Behind Vercel

Published: July 29, 2026
BullMQ Dashboard (Bull Board) Not Accessible Behind Vercel

Bull Board not working on Vercel shows up as one of two frustrating symptoms: the dashboard route returns a flat 404 as if it doesn't exist at all, or it throws a module resolution error naming a package — commonly @bull-board/ui — that's clearly present in your dependencies and works fine locally. Neither symptom means Bull Board is broken. Both mean it's running somewhere fundamentally mismatched with what it actually is: a persistent, stateful admin dashboard, deployed onto a platform built around short-lived, per-request functions.

Short answer: Bull Board is a continuously running Express application, and Vercel's serverless functions don't fit that model — the fix isn't a configuration tweak, it's hosting the dashboard on the same always-on service already running your BullMQ worker, not trying to force it into a Vercel function.

A high-tech command center with rows of illuminated digital screens, representing the Bull Board dashboard trying to run somewhere it fundamentally doesn't fit

Why Bull Board Doesn't Fit Vercel's Serverless Model

A real deployment thread on this exact problem traces the root cause directly: Bull Board's Express-based setup, including patterns like http.createServer(app), assumes a persistent server process — something that starts once and keeps running, holding its Redis connection open the whole time. Vercel's serverless functions are the opposite by design: each invocation can spin up fresh, with no guarantee of reusing anything from the last request. On top of that architectural mismatch, Bull Board's UI package ships bundled static assets in a way that can trigger Cannot find module '@bull-board/ui/package.json' specifically inside a serverless function's packaged environment — a module resolution quirk that has nothing to do with your code being wrong and everything to do with the deployment shape being wrong for what this package is.

(If this feels familiar, it's the same underlying shape as WebSockets not working on Vercel — anything that expects to be a long-lived, stateful process runs into the same wall on serverless functions, and a job queue dashboard is exactly that kind of process.)

The Fix: Host Bull Board on the Same Always-On Service as Your Worker

If you already have a BullMQ worker running as its own always-on service — as our guide on jobs stuck waiting on Render's Background Workers covers — that service already holds the persistent Redis connection Bull Board needs. Mounting the dashboard there, rather than reaching for Vercel at all, is the simplest fix:

TypeScript
1// worker-service/dashboard.ts — Bull Board mounted alongside the worker, on an always-on service
2import express from 'express';
3import { Queue } from 'bullmq';
4import { createBullBoard } from '@bull-board/api';
5import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
6import { ExpressAdapter } from '@bull-board/express';
7
8const emailQueue = new Queue('emails', { connection: { host: process.env.REDIS_HOST, port: 6379 } });
9
10const serverAdapter = new ExpressAdapter();
11serverAdapter.setBasePath('/admin/queues');
12
13createBullBoard({
14  queues: [new BullMQAdapter(emailQueue)],
15  serverAdapter,
16});
17
18const app = express();
19app.use('/admin/queues', serverAdapter.getRouter());
20app.listen(3001, () => console.log('Dashboard running on port 3001'));

This runs as a genuinely continuous process, exactly what Bull Board expects. Bull Board's own repository documents the full range of supported adapters if your worker service isn't running Express specifically. Your main API and frontend can stay on Vercel without any conflict — only the dashboard, which needs a persistent connection to actually function, moves to the service that's already built for that.

An empty outdoor street market with white tents and no vendors present, representing the ephemeral, per-invocation nature of serverless functions that a persistent dashboard can't rely on

Securing the Dashboard Before It's Reachable From the Internet

Bull Board's queue view includes job payloads and error details — not something to leave open to anyone who finds the URL. Wrap the route in basic authentication before it's live anywhere public:

TypeScript
1// worker-service/dashboard.ts — adding basic auth to the mounted dashboard route
2import basicAuth from 'express-basic-auth';
3
4app.use(
5  '/admin/queues',
6  basicAuth({
7    users: { admin: process.env.BULL_BOARD_PASSWORD! },
8    challenge: true,
9  }),
10  serverAdapter.getRouter(),
11);

express-basic-auth's own documentation covers additional options like per-user credentials and custom unauthorized responses if a single shared password isn't granular enough for your team. This is a minimum, not a complete auth strategy — but it's the difference between a dashboard reachable only by whoever has the credentials versus one openly browsable by anyone who guesses the route.

A close-up of an illuminated security keypad mounted on a wall, representing the basic authentication that should sit in front of the Bull Board dashboard before it's exposed publicly

The Opinion Part

Here's the pattern worth naming, because it's the same one underneath nearly every "this doesn't work on Vercel" bug in this genre: serverless functions are an excellent fit for stateless request-response work, and a genuinely bad fit for anything that needs to persist a connection, hold state in memory, or run continuously in the background — a WebSocket gateway, a queue worker, or an admin dashboard watching that same queue in real time. The fix is never a clever workaround that makes the wrong architecture technically function. It's recognizing which pieces of your stack are stateless (put them on Vercel) and which are inherently stateful (put them somewhere built for that), and stopping the habit of assuming "our whole app is on Vercel" has to mean literally every component lives there.

Conclusion

If Bull Board is 404ing or throwing module errors on Vercel, the dashboard isn't broken — it's deployed onto a platform that was never going to run a persistent Express app reliably. Move it to the same always-on service already running your BullMQ worker, wrap the route in basic authentication before it's reachable from the internet, and let Vercel keep handling the stateless parts of your stack it's actually good at.

If Redis connections themselves are dropping during deploys on the same always-on service, our Redis reconnection guide covers that adjacent piece of the same stack.

Move the dashboard somewhere built to run continuously, and enjoy actually being able to see your queue instead of staring at a 404 where a job list should be.

Frequently Asked Questions

Two separate issues commonly combine here. First, Bull Board is built as a persistent Express (or similar) application expecting to run continuously, which conflicts with Vercel's per-invocation serverless function model — the same underlying mismatch that breaks WebSockets and background workers on Vercel. Second, Bull Board's UI assets are bundled in a way that can trigger module resolution errors specifically inside a serverless function's packaged environment, even when the same code runs fine locally.

Host Bull Board on the same always-on service that's already running your BullMQ worker — Railway, Render, or a small dedicated instance — rather than trying to make it work as a Vercel serverless function. Vercel can still host your main application; the dashboard just needs to live somewhere built for a continuously running process, since that's what it fundamentally is.

Not necessarily a separate one — the cleanest setup mounts Bull Board's router directly on the same always-on service already running your BullMQ worker, since that service already holds the persistent Redis connection Bull Board needs. Adding one more route to an existing always-on service is simpler than standing up a dedicated deployment just for a dashboard.

At minimum, wrap the route in HTTP basic authentication using a package like express-basic-auth, so the dashboard isn't reachable by anyone who happens to guess or find the URL. Bull Board's queue data includes job payloads and error details, which can contain information you don't want publicly browsable, so this isn't optional polish once the dashboard is live anywhere reachable from the internet.

Yes, as long as Bull Board connects to the same Redis instance your queues and workers use — it doesn't need to run on the same physical service as your API, only somewhere continuously running with network access to that Redis connection. Mounting it alongside the worker is usually simplest, but it's a Redis-connection requirement, not a same-service requirement.

Portrait of Umar Farooq

About Umar Farooq

Umar Farooq is the founder and lead engineer of Codify SaaS. He builds B2B SaaS products and web applications on modern TypeScript stacks and enterprise Java, and writes code-first guides drawn from real production work — the schema decisions, the migrations that almost went wrong, and the performance fixes that actually moved the numbers. When he recommends an approach, he shows the code and explains the trade-offs.

Read full bio