Skip to content
Katabench
Try free
Course preview Katabench Labs for .NET developers

Build an async API in ASP.NET Core.
Then prove it survives retries and crashes.

A report endpoint takes seconds, so clients give up while the server keeps working. Turn a small .NET minimal API on real PostgreSQL into a proper async API: 202 Accepted and a status URL, a bounded queue and background worker, polite polling, idempotency keys, crash recovery, retries, problem details, two instances sharing work with SKIP LOCKED, and signed webhooks. About 85 minutes.

Intermediate · C#, ASP.NET Core, PostgreSQL · About 85 minutes · Disposable browser workspace · Pro workspace in private beta

A client POSTs a report request and gets 202 Accepted with a status URL; the job waits in a PostgreSQL table, background workers on two API instances claim it, the client polls with Retry-After until a 303 points at the report, and a signed webhook can announce it instead.

Outcomes

What you will be able to do

  • Answer a slow request with 202 Accepted, a Location and Retry-After, and serve status and result as separate resources
  • Run jobs in a BackgroundService behind a bounded Channel, with backpressure as 503 and Retry-After
  • Keep jobs in PostgreSQL so retries, crashes and restarts never lose or duplicate one, using idempotency keys and recovery
  • Retry transient failures with backoff, time out hung work, and report failures as RFC 9457 problem details
  • Share work across instances with FOR UPDATE SKIP LOCKED and leases, and push results with signed webhooks

11 lessons in 3 modules

One endpoint, from blocking to async

Each lesson starts from the code the previous one left behind. A scenario shows the failure with real clients, you change a few lines, and the same scenario proves the fix.

Module 1

Accept now, work later

Answer a slow request with 202 Accepted, run the work in the background, and let clients poll for the result.

5 lessons · 37 minutes

  1. Lesson 1 7 min

    Return 202 Accepted

    Stop holding the request open: accept the job, answer 202 with a status URL, and build the report in the background.

  2. Lesson 2 8 min

    Queue the work with a Channel

    Replace fire-and-forget with a bounded Channel and a BackgroundService that runs two reports at a time.

  3. Lesson 3 8 min

    Store jobs in PostgreSQL

    Move the jobs out of process memory into a table, so a restart forgets nothing and the channel carries only ids.

  4. Lesson 4 7 min

    Redirect to the result

    Split the status resource from the result: 200 while running, 303 See Other when done, and a cacheable report URL.

  5. Lesson 5 7 min

    Poll politely

    Fix a client that hammers the status URL: honor Retry-After within limits, follow the 303, and give up at a deadline.

Module 2

Make it reliable

Survive retried requests, crashes, flaky dependencies and hung work without losing or duplicating a job.

4 lessons · 31 minutes

  1. Lesson 6 8 min

    Make retries safe with idempotency keys

    A retried POST must not create a second job: store the Idempotency-Key and let a unique index decide.

  2. Lesson 7 7 min

    Recover after a crash

    kill -9 the API mid-work, then make the table the source of truth: requeue every unfinished job at startup.

  3. Lesson 8 8 min

    Retry transient failures

    One exception should not stop the API: catch it per job, retry a transient failure with backoff and jitter, fail a permanent one fast.

  4. Lesson 9 8 min

    Report failures with problem details

    Reject bad input with a 400 problem, give a failed job a problem document clients can branch on, and time out hung work.

Module 3

Scale beyond one process

Share the work between instances through the table, then push the result to the client instead of waiting to be asked.

2 lessons · 17 minutes

  1. Lesson 10 9 min

    Claim jobs with SKIP LOCKED

    Run two instances: claim jobs from the table with FOR UPDATE SKIP LOCKED and a lease, so work is shared and a dead instance's job moves on.

  2. Lesson 11 8 min

    Notify with a webhook

    Push instead of poll: POST a signed notification to the client's callbackUrl, retry it, and let the receiver verify it.

Build an async API: 202 Accepted, background jobs, and polling

1 lab · 11 lessons · about 85 minutes · Intermediate

See the lesson loop

The lesson loop

Focused changes, proven by real clients

The API and its client are open in the editor: Program.cs, the job store, the queue and worker, the webhook sender, and the polling client. Every command is given; you write the few lines of C# that carry the pattern. If you get stuck, Reveal solution loads the working code for the lesson.

Api/JobStore.cs
public async Task<Guid?> ClaimNextAsync(TimeSpan lease, CancellationToken ct = default)
{
    await using var command = db.CreateCommand("""
        UPDATE jobs SET status = 'running', locked_until = now() + $1, updated_at = now()
        WHERE id = (
            SELECT id FROM jobs
            WHERE status = 'queued' OR (status = 'running' AND locked_until < now())
            ORDER BY created_at
            FOR UPDATE SKIP LOCKED
            LIMIT 1)
        RETURNING id
        """);
    command.Parameters.AddWithValue(lease);
    return await command.ExecuteScalarAsync(ct) as Guid?;
}
  1. 1

    Reproduce

    Run the lesson's scenario. Real clients call your API, and the summary says what went wrong: timeouts, wasted reports, a second job for one request.

  2. 2

    Inspect

    Read the counts behind the failure, and query the jobs table in PostgreSQL when you want to see the rows.

  3. 3

    Patch

    Make one focused change of a few lines in the files the lesson names.

  4. 4

    Prove

    Press Check. It starts your API from the current code and reruns the scenario, so an old passing run cannot pass the step.

Executable evidence

The check reruns the scenario against your code

HTTP

202 with Location and Retry-After, 200 while running, 303 See Other to a cacheable report, and problem details for bad input and failed jobs.

PostgreSQL

Jobs and reports survive a restart and a kill -9, and a repeated Idempotency-Key gets the job it already has.

Workers

Two reports run at a time, four wait, the next POST gets 503, and a transient failure retries with backoff.

Scale and push

Two instances share jobs through the table, take over a dead instance's job, and send webhooks signed with HMAC-SHA256.

More Labs courses

Build another system

  • Implement the Transactional Outbox pattern

    Store the event with the order, relay it to RabbitMQ, and make duplicates and poison messages harmless.

    4 lessons · about 30 minutes

    See the course
  • Build a distributed app with the Aspire CLI

    Grow one API into an app with PostgreSQL, Redis, a worker, and telemetry, then publish it as Docker Compose.

    12 lessons · about 90 minutes

    See the course

Get notified when Labs opens

Live Pro workspaces are in private beta. The opening is announced in the Katabench newsletter, a short note when fresh kata land. No spam, unsubscribe anytime.

Try a graded kata while you wait