Private services
A private service is an always-on service with no public domain. Same canvas tile and build path as public HTTP; it just is not exposed to the internet. Use it for queue consumers, processors, and other private workloads.
When to leave a service private
Section titled “When to leave a service private”Skip generating a domain when the process does not serve public HTTP:
- Queue consumers (BullMQ, Celery, Sidekiq, NATS, RabbitMQ)
- Event stream processors (Kafka, Redis Streams)
- Data sync and ETL pipelines
- Email / notification dispatchers
- Webhook fanout processors
- Machine learning inference workers
Build pipeline
Section titled “Build pipeline”Workers use the exact same build pipeline as web services. StackBlaze detects your runtime, runs the install and build commands, and packages the result into a Docker image. The only difference is the run configuration — workers are deployed with no public ingress.
This means you can share a monorepo between a web service and its worker. They build from the same code but run different start commands.
Restart policy
Section titled “Restart policy”Workers must run continuously. If your worker process exits (due to an unhandled exception or crash), StackBlaze restarts it with an exponential backoff: 10s, 20s, 40s, up to a maximum of 5 minutes. After an instance runs successfully for 10 minutes, the backoff counter resets.
Environment variables
Section titled “Environment variables”Workers have full access to the project’s environment variables. Commonly you’ll want:
| Variable | Example |
|---|---|
REDIS_URL |
redis://cache:6379 |
DATABASE_URL |
postgresql://user:pass@postgres:5432/mydb |
WORKER_CONCURRENCY |
5 |
QUEUE_NAME |
email-dispatch |
BullMQ + Redis example (Node.js)
Section titled “BullMQ + Redis example (Node.js)”BullMQ is a popular Node.js queue library backed by Redis. Here is a complete worker that processes jobs from an email queue:
import { Worker } from 'bullmq'import { createTransport } from 'nodemailer'
const redisConnection = { host: process.env.REDIS_HOST || 'cache', port: parseInt(process.env.REDIS_PORT || '6379', 10),}
const transporter = createTransport({ host: process.env.SMTP_HOST, port: 587, auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, },})
const worker = new Worker( 'email-dispatch', async (job) => { const { to, subject, html } = job.data console.log(`[worker] Sending email to ${to} (job ${job.id})`) await transporter.sendMail({ from: 'no-reply@acme.com', to, subject, html }) console.log(`[worker] Done: job ${job.id}`) }, { connection: redisConnection, concurrency: parseInt(process.env.WORKER_CONCURRENCY || '5', 10), })
worker.on('failed', (job, err) => { console.error(`[worker] Job ${job?.id} failed:`, err.message)})
// Keep the process aliveprocess.on('SIGTERM', async () => { await worker.close() process.exit(0)})Corresponding web service (enqueue jobs)
Section titled “Corresponding web service (enqueue jobs)”import { Queue } from 'bullmq'
const emailQueue = new Queue('email-dispatch', { connection: { host: process.env.REDIS_HOST || 'cache', port: 6379, },})
// Enqueue from your API handlerapp.post('/send-welcome-email', async (req, res) => { await emailQueue.add('welcome', { to: req.body.email, subject: 'Welcome to Acme!', html: '<h1>Welcome!</h1>', }) res.json({ queued: true })})Celery + Redis example (Python)
Section titled “Celery + Redis example (Python)”from celery import Celeryimport os
redis_url = os.environ.get('REDIS_URL', 'redis://cache:6379/0')
app = Celery('tasks', broker=redis_url, backend=redis_url)
@app.taskdef process_order(order_id: str) -> dict: # heavy processing here print(f"Processing order {order_id}") return {"status": "processed", "order_id": order_id}
# Start command: celery -A worker worker --loglevel=info --concurrency=4Scaling workers
Section titled “Scaling workers”Workers can be scaled horizontally just like web services — increase the replica count to process more jobs in parallel. Each replica runs an independent worker process and pulls from the same queue. Queue libraries like BullMQ and Celery handle concurrent access safely via Redis atomic operations.
From the dashboard, go to Service → Settings → Scaling and set the replica count.
Monitoring worker health
Section titled “Monitoring worker health”Workers don’t have HTTP health checks (no port to probe). StackBlaze monitors the process exit code instead. You can view worker logs in real time from the dashboard, or search history with search_logs in MCP / chat:
# project / environment / servicecurl -sS "https://api.stackblaze.cloud/api/logs/{pipeline}/{phase}/{app}/" \ -H "Authorization: Bearer $STACKBLAZE_TOKEN"For deeper observability, emit structured JSON logs and open Coroot from the service.
Related
Section titled “Related”If the work should run on a schedule, use a cron service. For disk that survives restarts, attach a volume.