Troubleshooting
Common issues when getting started with Stem and how to resolve them.
Worker starts but no tasks are processed
Checklist:
- Make sure the producer and worker share the same broker URL.
- Confirm the worker is subscribed to the queue you are enqueueing into.
- If routing is enabled, verify the routing file and default queue.
- Minimal task handler
- Worker + broker bootstrap
- Producer enqueue
- Read results from the backend
lib/troubleshooting.dart
class EchoTask extends TaskHandler<String> {
String get name => 'debug.echo';
TaskOptions get options => const TaskOptions(queue: 'default');
Future<String> call(TaskContext context, Map<String, Object?> args) async {
final message = args['message'] as String? ?? 'hello';
print('echo: $message');
return message;
}
}
lib/troubleshooting.dart
final app = await StemApp.inMemory(
tasks: [EchoTask()],
workerConfig: const StemWorkerConfig(
queue: 'default',
consumerName: 'troubleshooting-worker',
concurrency: 1,
),
);
unawaited(app.start());
lib/troubleshooting.dart
final taskId = await app.stem.enqueue(
'debug.echo',
args: {'message': 'troubleshooting'},
);
lib/troubleshooting.dart
await Future<void>.delayed(const Duration(milliseconds: 200));
final result = await app.backend.get(taskId);
print('Result: ${result?.payload}');
Helpful commands:
stem worker stats --json
stem worker inspect
stem observe queues
Routing file fails to parse
Checklist:
- Validate the routing file path and format (YAML/JSON).
- Confirm
STEM_ROUTING_CONFIGpoints at the file you expect. - Confirm the registry matches the task names referenced in the file.
- If you use queue priorities, ensure the broker supports them.
- Load routing file
- Inline routing registry
lib/routing.dart
Future<RoutingRegistry> loadRouting() async {
final source = await File('config/routing.yaml').readAsString();
return RoutingRegistry.fromYaml(source);
}
final registry = RoutingRegistry(
RoutingConfig(
defaultQueue: const DefaultQueueConfig(alias: 'default', queue: 'primary'),
queues: {'primary': QueueDefinition(name: 'primary')},
routes: [
RouteDefinition(
match: RouteMatch.fromJson(const {'task': 'reports.*'}),
target: RouteTarget(type: 'queue', name: 'primary'),
),
],
),
);
lib/routing.dart
final inlineRegistry = RoutingRegistry(
RoutingConfig(
defaultQueue: const DefaultQueueConfig(alias: 'default', queue: 'primary'),
queues: {'primary': QueueDefinition(name: 'primary')},
routes: [
RouteDefinition(
match: RouteMatch.fromJson(const {'task': 'reports.*'}),
target: RouteTarget(type: 'queue', name: 'primary'),
),
],
),
);
Helpful commands:
stem routing dump
stem routing dump --json
stem routing dump --sample
Missing or misconfigured result backend
Symptoms: stem observe fails or task results never appear.
Checklist:
- Set
STEM_RESULT_BACKEND_URLfor any workflow that needs stored results. - Ensure the backend URL uses the correct scheme (
redis://,postgres://). - Confirm the worker is configured with the same result backend.
- Redis result backend
- Postgres result backend
lib/persistence.dart
Future<void> connectRedisBackend() async {
final backend = await RedisResultBackend.connect('redis://localhost:6379/1');
final broker = await RedisStreamsBroker.connect('redis://localhost:6379');
final stem = Stem(
broker: broker,
registry: registry,
backend: backend,
);
await stem.enqueue('demo', args: {});
await backend.close();
await broker.close();
}
lib/persistence.dart
Future<void> connectPostgresBackend() async {
final backend = await PostgresResultBackend.connect(
connectionString: 'postgres://postgres:postgres@localhost:5432/stem',
);
final broker = await RedisStreamsBroker.connect('redis://localhost:6379');
final stem = Stem(
broker: broker,
registry: registry,
backend: backend,
);
await stem.enqueue('demo', args: {});
await backend.close();
await broker.close();
}
Helpful commands:
stem health --backend "$STEM_RESULT_BACKEND_URL"
stem observe workers
stem observe queues
TLS or signing failures
Symptoms: health checks fail or tasks land in the DLQ with signature errors.
Checklist:
- Verify
STEM_TLS_*variables are set on every component that connects. - Confirm
STEM_SIGNING_KEYS/STEM_SIGNING_PUBLIC_KEYSmatch across producers and workers. - Ensure
STEM_SIGNING_ACTIVE_KEYis set and present in the key list. - Check DLQ entries for
signature-invalidreasons.
- Signed enqueue
- Shared signer config
- Stem + worker signing setup
lib/producer.dart
Future<void> enqueueWithSigning() async {
final config = StemConfig.fromEnvironment();
final broker = await RedisStreamsBroker.connect(
config.brokerUrl,
tls: config.tls,
);
final backend = InMemoryResultBackend();
final registry = SimpleTaskRegistry()
..register(
FunctionTaskHandler<void>(
name: 'billing.charge',
entrypoint: (context, args) async {
final customerId = args['customerId'] as String? ?? 'unknown';
print('Queued charge for $customerId');
return null;
},
),
);
final stem = Stem(
broker: broker,
registry: registry,
backend: backend,
signer: PayloadSigner.maybe(config.signing),
);
await stem.enqueue(
'billing.charge',
args: {'customerId': 'cust_123', 'amount': 4200},
notBefore: DateTime.now().add(const Duration(minutes: 5)),
);
await backend.close();
await broker.close();
}
lib/production_checklist.dart
Future<void> configureSigning() async {
final config = StemConfig.fromEnvironment();
lib/production_checklist.dart
// #region production-signing-stem
final stem = Stem(
broker: broker,
backend: backend,
registry: registry,
signer: signer,
);
// #endregion production-signing-stem
// #region production-signing-worker
final worker = Worker(
broker: broker,
backend: backend,
registry: registry,
signer: signer,
);
// #endregion production-signing-worker
Helpful commands:
stem health \
--broker "$STEM_BROKER_URL" \
--backend "$STEM_RESULT_BACKEND_URL"
stem dlq list --queue <queue>
stem dlq show --queue <queue> --id <task-id>
Namespace mismatch
Symptoms: CLI sees no data or control commands return empty responses.
Checklist:
- Ensure all processes (producer, worker, CLI) use the same namespace string.
- For workers, confirm
STEM_WORKER_NAMESPACEmatches your CLI--namespace.
- Broker + backend namespace
- Worker namespace
lib/namespaces.dart
Future<void> configureNamespace() async {
final broker = await RedisStreamsBroker.connect(
'redis://localhost:6379/0',
namespace: 'prod-us-east',
);
final backend = await RedisResultBackend.connect(
'redis://localhost:6379/1',
namespace: 'prod-us-east',
);
await broker.close();
await backend.close();
}
lib/namespaces.dart
Future<void> configureWorkerNamespace() async {
final registry = SimpleTaskRegistry();
final broker = InMemoryBroker(namespace: 'prod-us-east');
final backend = InMemoryResultBackend();
final worker = Worker(
broker: broker,
registry: registry,
backend: backend,
heartbeatNamespace: 'prod-us-east',
);
await worker.shutdown();
await backend.close();
await broker.close();
}
Helpful commands:
stem worker stats --namespace "stem"
stem worker ping --namespace "stem"
Migrations or schema errors
Checklist:
- Run the migration commands shipped with the adapter (Redis/Postgres).
- Ensure your store URLs point to the migrated database/schema.
- Set
STEM_SCHEDULE_STORE_URLbefore running schedule commands.
Helpful commands:
stem schedule list
DLQ stalls or poison-pill tasks
Checklist:
- Inspect DLQ entries and replay only after fixing the root cause.
- For repeat failures, consider lowering retries or adding task-level guards.
Helpful commands:
stem dlq list --queue <queue>
stem dlq show --queue <queue> --id <task-id>
stem dlq replay --queue <queue> --id <task-id>
Example enqueue that will land in the DLQ:
bin/producer.dart
for (final invoice in invoices) {
final id = await stem.enqueue(
taskName(),
args: {
'invoiceId': invoice,
},
meta: {
'createdAt': DateTime.now().toIso8601String(),
},
options: const TaskOptions(
queue: 'default',
maxRetries: 2,
),
);
stdout.writeln('[producer] queued invoice=$invoice taskId=$id');
}
Control commands return no replies
This usually means the control broadcast channel is not being consumed.
Checklist:
- Ensure the worker is running and connected to the same broker.
- If you use a custom namespace, pass
--namespaceto CLI commands. - Verify that the broker supports broadcast/control channels.
Helpful commands:
stem worker stats --json
stem observe workers
Task retries instantly or too quickly
Checklist:
- Confirm your task’s
TaskOptions(maxRetries,visibilityTimeout). - Ensure the broker supports delayed deliveries (
notBefore). - Check broker clock drift if delays feel inconsistent.
- Retry-related task options
- Jittered retry strategy
lib/retry_backoff.dart
TaskOptions get options => const TaskOptions(maxRetries: 2);
lib/retry_backoff.dart
final RetryStrategy retryStrategy = ExponentialJitterRetryStrategy(
base: const Duration(milliseconds: 200),
max: const Duration(seconds: 2),
);
Connection refused
Checklist:
- Verify Redis/Postgres is running and reachable.
- Confirm the URL scheme (
redis://,postgres://). - Ensure Docker ports are mapped (
-p 6379:6379,-p 5432:5432).
Helpful commands:
stem health \
--broker "$STEM_BROKER_URL" \
--backend "$STEM_RESULT_BACKEND_URL" \
Still stuck?
- Review the Observability & Ops guide for heartbeats, DLQ inspection, and control commands.
- Check the runnable examples under
packages/stem/example/.