KubeJob is a .NET library for work that should outlive the request that created it. It gives you typed handlers, retries, leases, cancellation, and an operator-facing dashboard for database-backed jobs. When a message broker is the right place to own delivery, the same handler model can consume broker-native jobs and events.
Quick start · Pick a runtime · Local development · Benchmarking · Samples · Documentation · 中文入口
The repository includes a PostgreSQL and RabbitMQ development stack. With Podman installed, start it with:
KUBEJOB_CONTAINER_ENGINE=podman bash scripts/dev-stack.sh upThe script also works with Docker Compose and selects an available engine when
KUBEJOB_CONTAINER_ENGINE is not set. You need the .NET 10 SDK to build and
test the repository.
Run the sample host in another terminal:
bash scripts/run-unified-sample.shOpen the dashboard at http://localhost:5041/admin/jobs. To create a few successful, retried, timed-out, and cancelled jobs, run:
bash scripts/seed-dashboard-demo.shSee Local development for Windows commands, service credentials, integration tests, and cleanup.
KubeJob has two deliberately separate ways to execute work. Pick one per queue; a queue is not partly managed by PostgreSQL and partly managed by the broker.
| Use | PostgresManaged | BrokerNative |
|---|---|---|
| State lives in | PostgreSQL | Message broker |
| Good for | Business jobs that need status, retries, schedules, cancellation, or operator actions | Integration events, notifications, and high-volume consumers |
| Delivery record | JobRun, attempts, leases, and completion are stored by KubeJob |
Delivery and acknowledgement are stored by the broker |
| Idempotency | KubeJob can enforce an IdempotencyKey |
Use a business key in the handler; no KubeJob de-duplication store is written |
| Failure handling | KubeJob retry and lease recovery | Broker retry and dead-letter policy |
Both paths are at-least-once. A successful acknowledgement cannot make an email, payment, or HTTP call exactly-once, so handlers that touch another system must be safe to run again.
Use this path when the job is part of a business process that someone may need to inspect or repair later. A worker claims a lease, runs the handler, and records the result in PostgreSQL. If a worker disappears, another worker can recover the expired lease.
Register a typed job:
public sealed record SendWelcomeEmail(string Address);
public sealed class SendWelcomeEmailJob : IKubeJob<SendWelcomeEmail>
{
public ValueTask ExecuteAsync(
SendWelcomeEmail payload,
JobExecutionContext context,
CancellationToken cancellationToken)
{
// Send the email here and pass cancellationToken to the mail client.
return ValueTask.CompletedTask;
}
}
var key = new JobKey<SendWelcomeEmail>("mail.send-welcome");
builder.Services.AddKubeJobHandler<SendWelcomeEmailJob, SendWelcomeEmail>(key);For a small service, the control plane and worker can run in the same process:
builder.Services.AddKubeJob(
configureServer: server => server.UsePostgreSql(connectionString),
configureWorker: worker =>
{
worker.WorkerId = Environment.MachineName;
worker.BuildId = "mail-service";
worker.MaxConcurrentJobs = 16;
worker.Queues = new List<string> { "mail.send-welcome" };
});Enqueue through IJobClient. The idempotency key is stored with the managed
job, so a repeated request can resolve to the same logical submission:
await jobs.EnqueueAsync(
key,
new SendWelcomeEmail("user@example.com"),
new JobEnqueueOptions
{
IdempotencyKey = "welcome:user-42",
MaxAttempts = 5,
Timeout = TimeSpan.FromMinutes(2)
});Use this path when the broker should own delivery. KubeJob publishes to the
configured transport and the consumer acknowledges only after the handler
finishes. No managed JobRun, lease, or completion row is created for this
path.
RabbitMQ is the included adapter; Kafka is an alternative BrokerNative adapter. Choose one transport id per logical job queue:
builder.Services.AddKubeJobServer();
builder.Services.ConfigureKubeJobQueueRuntimes(options =>
{
options.Queues["order.created"] = new QueueRuntimeRoute
{
Mode = QueueRuntimeMode.BrokerNative,
TransportId = RabbitMqBrokerNativePublisher.Id
};
});
builder.Services.AddRabbitMqKubeJobBrokerNativeTransport(options =>
options.ConnectionString = rabbitMqConnectionString);
var orderCreatedKey = new JobKey<OrderCreated>("order.created");
builder.Services.AddKubeJobHandler<OrderCreatedJob, OrderCreated>(orderCreatedKey);
builder.Services.AddKubeJobBrokerNativeWorker(options =>
{
options.WorkerId = Environment.MachineName;
options.BuildId = "order-consumer";
options.Queues = new List<string> { "order.created" };
});
builder.Services.AddRabbitMqKubeJobBrokerNativeConsumer(options =>
options.ConnectionString = rabbitMqConnectionString);BrokerNative event consumers additionally need PostgreSQL for their durable
Inbox. Configure the server with server.UsePostgreSql(connectionString) and
initialize its schema before starting the consumer. The Inbox records a
successful (EventId, capability) before broker acknowledgement, preventing a
redelivery after that write from invoking the same capability again.
Kafka keeps the same Core contract, but maps a job queue to a Kafka topic and maps the fixed event capabilities to three consumer groups. Replicas in the same group distribute partitions horizontally:
using KubeJob.Transport.Kafka;
builder.Services.ConfigureKubeJobQueueRuntimes(options =>
{
options.Queues["order.created"] = new QueueRuntimeRoute
{
Mode = QueueRuntimeMode.BrokerNative,
TransportId = KafkaBrokerNativePublisher.Id
};
});
builder.Services.AddKafkaKubeJobBrokerNativeTransport(options =>
options.BootstrapServers = "kafka-1:9092,kafka-2:9092");
builder.Services.AddKafkaKubeJobBrokerNativeConsumer(options =>
options.BootstrapServers = "kafka-1:9092,kafka-2:9092");For events Kafka uses one shared order.events topic and exactly three fixed
consumer groups: kubejob.<environment>.log, .data, and .notify. Register
event handlers with one of those subscription names, then call
AddKafkaKubeJobEventConsumer. Retry and dead-letter records stay scoped to
the capability group, so a data retry is not replayed to log or notify. Kafka
does not provide RabbitMQ-style per-message TTL; retries use visible 5s, 30s,
5m, or 30m tiers (larger requested delays are capped at 30m).
For RabbitMQ fan-out, give each event consumer a different subscription name.
Kafka deliberately restricts subscriptions to the fixed log, data, and
notify capabilities above. See Event subscriptions for
the topic, queue, retry, and dead-letter rules.
- HTTP and Dashboard endpoints require authorization by default. Add an
explicit policy and call
UseAuthentication()andUseAuthorization()in the host pipeline. AllowAnonymousEndpointsandAllowAnonymousAccessare intended for local development and tests. Do not enable them on a public or shared network.- Initialize the PostgreSQL schema before starting managed workers. The sample
does this with
InitializeKubeJobDatabase(). - Event consumers use the same PostgreSQL schema for their durable Inbox; they fail at startup if no durable Inbox is configured.
- RabbitMQ BrokerNative uses one fixed retry delay per retry queue. Configure
RetryDelayfor the adapter; KubeJob does not create per-delay or per-worker retry queues. - Kafka topic creation is disabled by default. Provision
order.events, each job topic, and their.retry/.dlqcompanions before startup; useCreateTopicsOnStartuponly for local development. - Do not put large payloads or secrets in a job message. Store sensitive data in your application database and pass a reference to the handler.
samples/KubeJob.Sample.Unified is the maintained end-to-end sample. It uses
PostgresManaged jobs and exposes the dashboard and demo endpoints. The
RabbitMQ integration tests under tests/KubeJob.RabbitMqIntegrationTests
exercise the BrokerNative job and event paths.
Start dependencies, then run the release test suite:
bash scripts/dev-stack.sh up
dotnet test KubeJob.sln -c ReleaseRabbitMQ integration tests run when
KUBEJOB_RABBITMQ_TEST_CONNECTION contains an AMQP connection string. Without
it, those tests are skipped intentionally.
Kafka integration tests run when KUBEJOB_KAFKA_TEST_BOOTSTRAP contains
bootstrap servers (for the local stack: localhost:9092). They are skipped
when Kafka is not configured.