Troubleshooting
Practical problem/solution guide for Pragmatic.Jobs. Each section covers a common issue, the likely causes, and the fix.
Job Not Executing
Section titled “Job Not Executing”You have a job class that compiles, but it never runs at runtime.
Checklist
Section titled “Checklist”-
Does the class have
[Job]or[RecurringJob]? Without the attribute, the SG does not discover the class. It compiles fine but has no invoker, no DI registration, and no type registry entry. -
Is the class
partial? DiagnosticPRAG2502warns if missing. Withoutpartial, the invoker is generated in a degraded mode. -
Does the class implement
IJoborIJob<T>? DiagnosticPRAG2500fires if the interface is missing. The SG will not generate an invoker. -
Is the SG analyzer referenced? In your
.csproj, thePragmatic.SourceGeneratormust be referenced withOutputItemType="Analyzer":<ProjectReference Include="..\Pragmatic.SourceGenerator\...\Pragmatic.SourceGenerator.csproj"OutputItemType="Analyzer"ReferenceOutputAssembly="false" /> -
Are processing services registered?
With Composition (automatic):
await PragmaticApp.RunAsync(args, app =>{app.UseJobs();});Without Composition (manual — both calls are required):
builder.Services.AddPragmaticJobs();builder.Services.AddJobProcessingServices(); // Registers the BackgroundServices -
Check the SG output. In Visual Studio, expand Dependencies > Analyzers > Pragmatic.SourceGenerator in Solution Explorer. Look for
{JobType}.Invoker.g.csand_Infra.Jobs.Registration.g.cs. If these files do not exist, the SG is not processing your class. -
Is
NullJobTypeRegistryin use? If no jobs are discovered, the runtimeIJobTypeRegistryresolves toNullJobTypeRegistry, and bothJobProcessorServiceandRecurringJobSchedulerServiceexit immediately. Check that the SG-generatedPragmaticJobTypeRegistryis registered. -
Is the
ScheduledFortime in the future?JobProcessorServiceonly fetches jobs whereScheduledFor <= now. If you scheduled a delayed job, verify the timestamp. -
Check the logs.
JobProcessorServicelogs at startup:"Job processor started: worker={WorkerId}, concurrency={WorkerCount}". If you do not see this message, the service is not running. Look for"Job processor poll error"messages that indicate exceptions during polling.
Recurring Job Not Registered
Section titled “Recurring Job Not Registered”A recurring job exists in code but does not appear in __RecurringJobs and never fires.
Checklist
Section titled “Checklist”-
Does the class have
[RecurringJob("cron")]? Not[Job]— only[RecurringJob]generates entries in_Infra.Jobs.RecurringJobs.g.cs. -
Is the cron expression valid? An empty or malformed expression triggers
PRAG2501. Check the build output for this diagnostic. -
Check
_Infra.Jobs.RecurringJobs.g.cs. Open the SG output and verify your job appears in theRegisterRecurringJobsmethod. -
Is the recurring definition upserted at startup? The SG-generated registration calls
IRecurringJobStore.UpsertAsync()for each recurring definition. If the store is EF Core, check the__RecurringJobstable. -
Is
RecurringJobSchedulerServicerunning? Check the logs for"Recurring job scheduler started". If absent,AddJobProcessingServices()was not called. -
Is the timezone correct? If you specify
TimeZone = "America/New_York", the cron expression is evaluated in that timezone. A job scheduled for"0 2 * * *"in New York might not fire at the UTC time you expect. -
Is the definition disabled?
RecurringJobDefinition.IsEnableddefaults totrue, but if something callsIRecurringJobStore.DisableAsync(), the job will not be picked up.
Lease Conflicts (Multiple Workers)
Section titled “Lease Conflicts (Multiple Workers)”The pragmatic.jobs.lease_conflicts counter is high, meaning workers are competing for the same jobs.
Checklist
Section titled “Checklist”-
Is the batch size too large? If
BatchSize = 100andWorkerCount = 2, each poll fetches far more jobs than the workers can process before the next poll. ReduceBatchSizetoWorkerCount * 2orWorkerCount * 3. -
Is the polling interval too short? If multiple instances poll every second, lease conflicts increase. The default 5 seconds is usually sufficient.
-
Are leases expiring too fast? If
LeaseTimeSecondsis shorter than the typical job execution time, the lease expires mid-execution. Another worker picks up the same job. IncreaseLeaseTimeSecondsto be at least 2x the expected maximum job duration.
Diagnostics Reference
Section titled “Diagnostics Reference”The source generator emits the following diagnostics for common mistakes:
| ID | Severity | Title | Message |
|---|---|---|---|
| PRAG2500 | Error | Job class must implement IJob or IJob | Type ‘{0}’ is decorated with [Job]/[RecurringJob] but does not implement IJob or IJob |
| PRAG2501 | Error | Invalid cron expression | [RecurringJob] on ‘{0}’ has an empty or missing cron expression |
| PRAG2502 | Warning | Job class should be partial | Type ‘{0}’ is decorated with [Job]/[RecurringJob] but is not declared as partial |
| PRAG2503 | Error | Duplicate recurring job ID | Recurring job ID ‘{0}’ is used by both ‘{1}’ and ‘{2}‘ |
| PRAG2504 | Error | Invalid retry configuration | [Retry] on ‘{0}’ has MaxAttempts = {1}. MaxAttempts must be greater than 0 |
| PRAG2505 | Error | Continuation must be a job | [Continuation<{0}>] on ‘{1}’: type ‘{0}’ does not implement IJob or IJob |
| PRAG2506 | Error | Continuation cycle detected | Job ‘{0}’ creates a cycle in continuation chain: {1} |
Reading the Diagnostics
Section titled “Reading the Diagnostics”- PRAG2500: Add
: IJobor: IJob<TParams>to your class declaration. - PRAG2501: Provide a valid 5-part cron expression. Empty strings and expressions with the wrong number of fields are rejected.
- PRAG2502: Add the
partialkeyword to your class declaration. - PRAG2503: Specify a unique
Idin[RecurringJob(cron, Id = "unique-id")], or rely on auto-generated IDs by omitting theIdproperty. - PRAG2504: Set
MaxAttemptsto a positive integer. If you want no retry, omit[Retry]entirely. - PRAG2505: The type argument in
[Continuation<T>]must implementIJoborIJob<T>. Check the target class declaration. - PRAG2506: Remove
[Continuation<T>]from one of the jobs in the cycle to break it. The diagnostic message shows the full chain path.
Q: Can I use [RecurringJob] and [Job] on the same class?
Section titled “Q: Can I use [RecurringJob] and [Job] on the same class?”No. A class should have either [RecurringJob] (for cron-based scheduling) or [Job] (for on-demand scheduling), not both. If you need the same job logic to run on a schedule and also on demand, extract the logic into a shared service and create two separate job classes.
Q: What happens if my job throws an exception?
Section titled “Q: What happens if my job throws an exception?”If [Retry] is configured, the generated invoker catches the exception and retries with the configured backoff strategy. After all attempts are exhausted, JobProcessorService marks the job as Failed in the store. The error message is stored in JobInstance.Error.
If [Retry] is not configured, the default MaxAttempts from JobsOptions.DefaultMaxRetries applies (default: 1, meaning no retry). The job is marked Failed on the first exception.
Q: How do I cancel a scheduled job?
Section titled “Q: How do I cancel a scheduled job?”Use IJobScheduler.CancelAsync(jobId). The jobId is the Guid returned by ScheduleAsync or ScheduleAtAsync. This marks the job as Cancelled in the store. If the job is already running, cancellation depends on whether your ExecuteAsync observes the CancellationToken.
var jobId = await scheduler.ScheduleAsync<SendReminderJob, ReminderParams>( new ReminderParams(reservationId, email), delay: TimeSpan.FromHours(24));
// Later, if the reservation is cancelled:await scheduler.CancelAsync(jobId);Q: Can I have multiple workers processing different job types?
Section titled “Q: Can I have multiple workers processing different job types?”The current architecture uses a single JobProcessorService that processes all job types. Workers are generic — any worker can process any job. This is by design: the lease mechanism ensures each job is processed by exactly one worker. If you need dedicated workers for specific job types, deploy separate app instances with different job assemblies.
Q: How does the InMemory store behave with multiple workers?
Section titled “Q: How does the InMemory store behave with multiple workers?”InMemoryJobStore and InMemoryRecurringJobStore are singletons within a single process. They work with WorkerCount > 1 (multiple tasks in the same process) but do NOT work across multiple processes or containers. For multi-instance deployments, use UseEfCore().
Q: What happens if the cron timezone is invalid?
Section titled “Q: What happens if the cron timezone is invalid?”RecurringJobSchedulerService calls TimeZoneInfo.FindSystemTimeZoneById(), which throws TimeZoneNotFoundException for invalid IANA names on systems that do not support them. On Linux, IANA names are natively supported. On Windows, .NET 10 supports IANA names via ICU. If you see timezone errors, ensure your runtime supports the specified timezone.
Q: How do I see what jobs are pending/running/failed?
Section titled “Q: How do I see what jobs are pending/running/failed?”Query IJobStore directly. In EF Core mode, query the __Jobs table:
-- Pending jobsSELECT * FROM __Jobs WHERE Status = 0 ORDER BY ScheduledFor;
-- Failed jobs with error detailsSELECT Id, JobType, Error, Attempt, MaxAttempts FROM __Jobs WHERE Status = 3;
-- Currently running (leased) jobsSELECT Id, JobType, LeasedBy, LeaseExpiresAt FROM __Jobs WHERE Status = 1;Q: My continuation job does not receive the parameters from the parent job. Why?
Section titled “Q: My continuation job does not receive the parameters from the parent job. Why?”Continuation jobs receive the CorrelationId from the parent job, not the parameters. If the continuation needs data from the parent job, either:
- Use
CorrelationIdto look up the data in a shared store. - Store the result of the parent job in a database and query it in the continuation.
Observability Debugging
Section titled “Observability Debugging”OpenTelemetry Spans Not Appearing
Section titled “OpenTelemetry Spans Not Appearing”-
Is the ActivitySource configured? Add
Pragmatic.Jobsto your OpenTelemetry tracing configuration:builder.Services.AddOpenTelemetry().WithTracing(tracing => tracing.AddSource("Pragmatic.Jobs")); -
Are spans being sampled out? Check your sampler configuration. With
AlwaysOnSampler, all spans are captured.
Metrics Not Appearing
Section titled “Metrics Not Appearing”-
Is the Meter configured? Add
Pragmatic.Jobsto your OpenTelemetry metrics configuration:builder.Services.AddOpenTelemetry().WithMetrics(metrics => metrics.AddMeter("Pragmatic.Jobs")); -
Metrics available:
pragmatic.jobs.enqueued,pragmatic.jobs.completed,pragmatic.jobs.failed,pragmatic.jobs.retried,pragmatic.jobs.duration,pragmatic.jobs.lease_acquisitions,pragmatic.jobs.lease_conflicts,pragmatic.jobs.recurring_triggered.
Job Stuck in Running State
Section titled “Job Stuck in Running State”A job appears as Running (Status = 1) in __Jobs but no worker is actively processing it.
Checklist
Section titled “Checklist”-
Did the worker crash mid-execution? When a worker crashes, the lease remains until
LeaseExpiresAt.JobProcessorServicecallsIJobStore.ReleaseExpiredLeasesAsync()at the start of each poll cycle. Once the lease expires, the job becomes eligible for reprocessing. -
Is
LeaseTimeSecondstoo long? The default is 300 (5 minutes). If the worker crashed, you must wait for the full lease duration before another worker picks up the job. For fast jobs, reduce the lease time. For long jobs, the lease should exceed the expected execution time. -
Check
LeasedByin the database. If the value matches a worker that no longer exists, wait forLeaseExpiresAtto pass. Alternatively, manually reset the job:UPDATE __JobsSET Status = 0, LeasedBy = NULL, LeaseExpiresAt = NULLWHERE Id = 'your-job-id' AND Status = 1; -
Is the job genuinely long-running? Check the
StartedAttimestamp. If the job started recently and your[Timeout]is high, it may still be executing normally.
Job Parameters Deserialization Failure
Section titled “Job Parameters Deserialization Failure”A parametric job (IJob<T>) fails with a JSON deserialization error.
Checklist
Section titled “Checklist”-
Is the parameter type JSON-serializable? The SG-generated
IJobTypeRegistryusesSystem.Text.Jsonfor serialization. Ensure your parameter record/class has a parameterless constructor or usesrecordtypes (which serialize correctly by default). -
Did the parameter type change after jobs were already enqueued? If you rename properties or change the type structure, already-enqueued jobs in
__Jobshave the old JSON format. They will fail on deserialization. Clear or migrate old job records. -
Are there unsupported types in the parameters? Avoid
interfaceproperties,Func<>,Expression<>, or circular references. Stick to simple records with primitive types,Guid,DateTimeOffset,string, anddecimal.
EF Core Migration Issues
Section titled “EF Core Migration Issues”Tables Not Created
Section titled “Tables Not Created”The __Jobs and __RecurringJobs tables are created by EfCoreJobStore and EfCoreRecurringJobStore entity configurations. If they do not appear:
-
Is
UseEfCore()called? Check yourUseJobsconfiguration:app.UseJobs(jobs => jobs.UseEfCore()); -
Was
EnsureCreatedor a migration run? The entity configurations are registered when the EF Core store is resolved. Ensure your database migration includes these tables. -
Check the connection string. The EF Core stores use the same
DbContextas your application. Verify the connection string points to the correct database.
Getting Help
Section titled “Getting Help”- Showcase examples: See
NoShowDetectionJobandSendCheckInReminderJobinexamples/showcase/src/Showcase.Booking/Infrastructure/Jobs/for working examples of both recurring and delayed jobs. - Integration tests:
examples/showcase/tests/Showcase.IntegrationTests/Jobs/contains tests for job infrastructure and EF Core stores. - Architecture: Read concepts.md for the full pipeline explanation and decision tree.
- Common mistakes: Read common-mistakes.md for the most frequent issues with code examples.