{
  "id": 15134323,
  "title": "The Retry That Fired After the Stream Ended",
  "url": "https://urgent.news/2026/10/17/the-retry-that-fired-after-the-stream-ended",
  "topic": "tech",
  "section": "Tech",
  "published": "2026-10-17T06:16:47.000Z",
  "source": {
    "name": "Dev.to",
    "slug": "dev-to",
    "url": "https://dev.to/datacpp_3670/the-retry-that-fired-after-the-stream-ended-1fp1"
  },
  "original_language": "en",
  "account": "A stream can succeed without looking successful to clients. Retry logic adds complications. The main question is: what did the retry wrapper actually accomplish? A synthetic example will illustrate. The failure is real and can be reproduced locally. The scenario involves one endpoint, client, and assertion.\n\nThe symptom: Submitting a job once results in two rows in the database without any exception appearing in logs. The client logs a retry because the stream ended without a terminal event. This does not necessarily indicate a failure. The server might have accepted the job and completed the work, but clients often misinterpret the absence of a done event as a request failure.\n\nTo build a server that closes cleanly, a FastAPI app can be created. It streams one progress event but does not send a terminal event. Upon POSTing to the /jobs endpoint, the job ID is recorded in a list. An events generator yields a progress event followed by a sleep of 0.05 seconds. However, no terminal event is sent before closing the response. The server is started using uvicorn, and curl is used to make a POST request to the endpoint. The raw stream output stops after the progress event, with no error or done event.\n\nThe client code attempts to implement a retry wrapper. It retries if a terminal event is missing. When running the test, it is observed that two POST requests are made instead of one. This indicates that the retry wrapper is duplicating the POST request. The test demonstrates that the bug lies in the retry wrapper.\n\nTo classify the failure, several questions must be answered in order. Did the server receive any bytes? Check the access log or an idempotency store. Did the failure occur before the first response byte? If so, a retry is usually safe. Did the stream break after the first byte? In this case, querying the job state instead of reposting is recommended. Did the stream close cleanly after a 200 response? Treat it as a success, then fix the contract. Did the client time out while the server continued working? Retrying in this scenario can lead to duplicated work.\n\nA decision table is provided to help determine whether a POST can be safely retried. If the failure point is the connection refused or 503 status before the body, it is safe to backoff and retry. If the stream closes mid-body without a terminal event, retrieving the job state via GET /jobs/{id} is the better action. Lastly, if the client times out while the server remains alive, polling the job state by key is advised.\n\nTo fix the contract, both the server and client must be modified. The server must always end the stream, while the client must refrain from blindly retrying non-idempotent POST requests. An idempotency key and a terminal event should be added to both the server and client. This will ensure that the generated retry wrapper performs as intended and does not duplicate server-side jobs.",
  "summary": "The Retry That Fired After the Stream Ended A stream can succeed and still look failed to your client. That gap is where retry logic doubles side effects. The current AI conversation is full of fatigue and agent demos. The useful question is smaller: what did the generated retry wrapper actually do? I'll use a synthetic repro, not a customer incident. The failure is real enough to run on your…",
  "key_points": [
    "Stream ends without terminal event, causing client to retry",
    "FastAPI app streams progress event, no terminal event sent",
    "Retry wrapper duplicates POST request, causing duplicate jobs"
  ],
  "editors_take": "Improperly configured retry logic transforms silent stream terminations into duplicate server-side jobs, necessitating a shift from blind retries toward state-based polling and stricter communication contracts.",
  "illustration": null,
  "coverage": {
    "outlets": 1,
    "also_reported_by": []
  },
  "ai_generated": true,
  "disclaimer": "Summaries, key points and the editor’s take are written by software from other outlets’ reporting and may contain errors — always check the linked original."
}