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.
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
Outcomes
11 lessons in 3 modules
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
Answer a slow request with 202 Accepted, run the work in the background, and let clients poll for the result.
5 lessons · 37 minutes
Lesson 1 7 min
Stop holding the request open: accept the job, answer 202 with a status URL, and build the report in the background.
Lesson 2 8 min
Replace fire-and-forget with a bounded Channel and a BackgroundService that runs two reports at a time.
Lesson 3 8 min
Move the jobs out of process memory into a table, so a restart forgets nothing and the channel carries only ids.
Lesson 4 7 min
Split the status resource from the result: 200 while running, 303 See Other when done, and a cacheable report URL.
Lesson 5 7 min
Fix a client that hammers the status URL: honor Retry-After within limits, follow the 303, and give up at a deadline.
Module 2
Survive retried requests, crashes, flaky dependencies and hung work without losing or duplicating a job.
4 lessons · 31 minutes
Lesson 6 8 min
A retried POST must not create a second job: store the Idempotency-Key and let a unique index decide.
Lesson 7 7 min
kill -9 the API mid-work, then make the table the source of truth: requeue every unfinished job at startup.
Lesson 8 8 min
One exception should not stop the API: catch it per job, retry a transient failure with backoff and jitter, fail a permanent one fast.
Lesson 9 8 min
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
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
Lesson 10 9 min
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.
Lesson 11 8 min
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
The lesson loop
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.
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?;
} 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.
Read the counts behind the failure, and query the jobs table in PostgreSQL when you want to see the rows.
Make one focused change of a few lines in the files the lesson names.
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
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.
Jobs and reports survive a restart and a kill -9, and a repeated Idempotency-Key gets the job it already has.
Two reports run at a time, four wait, the next POST gets 503, and a transient failure retries with backoff.
Two instances share jobs through the table, take over a dead instance's job, and send webhooks signed with HMAC-SHA256.
More Labs courses
Store the event with the order, relay it to RabbitMQ, and make duplicates and poison messages harmless.
4 lessons · about 30 minutes
See the courseGrow 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 courseLive 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.