Async APIs: The 202 Accepted + Polling Pattern for Long-Running Operations
Some API requests can't finish in time for a single HTTP response. Generating a report, transcoding a video, running a batch import — these take seconds or minutes, far longer than any client should hold a connection open for. If you try to do this work inside a normal request, you'll hit gateway timeouts, frustrated clients retrying half-finished jobs, and load balancers killing connections at 30 or 60 seconds. The fix is a well-established HTTP pattern: accept the work, hand back a receipt, and let the client poll for the result. Here's how to build it properly. The shape of the pattern The client POST s the job. The server validates it, enqueues it, and immediately returns 202 Accepted with a URL where the status lives. The client polls that status URL until the job is done (or failed ). When complete, the status response points to the finished resource. The key detail most implementations get wrong: 202 does not mean "success." It means "I accepted this and will work on it." The actual outcome arrives later. Step 1: Accept the job import express from " express " ; import { randomUUID } from " crypto " ; const app = express (); app . use ( express . json ()); const jobs = new Map (); // use Redis or a DB in production app . post ( " /v1/reports " , ( req , res ) => { const id = randomUUID (); jobs . set ( id , { status : " pending " , createdAt : Date . now (), result : null }); // Kick off work without blocking the response processReport ( id , req . body ). catch (( err ) => { jobs . set ( id , { status : " failed " , error : err . message }); }); res . status ( 202 ) . location ( `/v1/reports/ ${ id } ` ) . json ({ id , status : " pending " }); }); Notice the Location header. It tells the client exactly where to look — no need to construct the URL itself. Step 2: Expose a status endpoint app . get ( " /v1/reports/:id " , ( req , res ) => { const job = jobs . get ( req . params . id ); if ( ! job ) return res . status ( 404 ). json ({ error : " unknown job " })