Skip to content

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.

Public HTTP to private workers Enqueue on Redis · workers have no ingress · scale replicas to drain faster Internet HTTPS Public HTTP POST /send-welcome-email enqueues the job queue Redis cache :6379 private only consume Worker replica 1 no public domain Worker replica 2 no ingress Worker replica n scale to process more jobs same build as public HTTP · different start command · same Redis · no public exposure

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

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.

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.

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 is a popular Node.js queue library backed by Redis. Here is a complete worker that processes jobs from an email queue:

worker.js
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 alive
process.on('SIGTERM', async () => {
await worker.close()
process.exit(0)
})
api.js
import { Queue } from 'bullmq'
const emailQueue = new Queue('email-dispatch', {
connection: {
host: process.env.REDIS_HOST || 'cache',
port: 6379,
},
})
// Enqueue from your API handler
app.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 })
})
worker.py
from celery import Celery
import os
redis_url = os.environ.get('REDIS_URL', 'redis://cache:6379/0')
app = Celery('tasks', broker=redis_url, backend=redis_url)
@app.task
def 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=4

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.

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:

terminal
# project / environment / service
curl -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.

If the work should run on a schedule, use a cron service. For disk that survives restarts, attach a volume.