HTTP Ingress Timeout: Long-Running Request Termination¶
Status: Published
Experiment completed with real data on 2026-05-04.
1. Question¶
Container Apps ingress has a configurable idle timeout and an implicit request timeout. What is the maximum duration for a single HTTP request, and what HTTP status code does the client receive when the request exceeds the timeout — and does the container process receive a signal before the connection is dropped?
2. Why this matters¶
Applications that perform long-running synchronous operations (data export, batch processing, file conversion) may require HTTP requests that take minutes to complete. Container Apps ingress has an idle timeout that terminates connections that are idle (no data sent) for more than the configured period. If the application does not stream any response bytes during a long operation, the ingress drops the connection even though the application is actively processing the request. Understanding this behavior is essential for designing appropriate patterns (streaming, async with polling, or webhooks) for long operations.
3. Customer symptom¶
"Long-running API calls fail after exactly 240 seconds with a connection error" or "Export endpoint works for small datasets but times out for large ones" or "The request completes on the server but the client never receives the response."
4. Hypothesis¶
- H1: Container Apps ingress has an idle connection timeout (default: 240 seconds) that terminates connections where no data is sent by the server for that period. A long-running request that does not send response headers or body bytes until completion is terminated by the ingress after 240 seconds.
- H2: The container process is NOT notified before the connection is terminated — the application continues processing after the ingress has closed the connection. The completed result is silently discarded.
- H3: Streaming the response (sending HTTP headers immediately, then streaming body bytes periodically) keeps the connection alive past the 240-second idle timeout, allowing arbitrarily long responses.
- H4: The
ingress.timeoutproperty in the container app configuration controls the idle timeout (in seconds, maximum 3600).
5. Environment¶
| Parameter | Value |
|---|---|
| Service | Azure Container Apps |
| SKU / Plan | Consumption |
| Region | Korea Central |
| Environment | env-batch-lab |
| App name | aca-diag-batch |
| Runtime | Python 3.11 / Flask / gunicorn |
| Date tested | 2026-05-04 |
6. Variables¶
Experiment type: Networking / Platform behavior
Controlled:
/sleep?s=<n>Flask endpoint — sleeps for n seconds then returns JSON- gunicorn
--timeout 600to prevent worker timeout before the ingress fires
Observed:
- HTTP status code returned at timeout boundary
- Response body at timeout
- Exact timing of connection termination
Scenarios:
- S1: 10-second sleep → verify baseline works
- S2: 60-second sleep → verify well within timeout
- S3: 230-second sleep → verify just under the boundary
- S4: 240-second sleep → observe timeout
- S5: 241-second sleep → confirm consistent 240s boundary
7. Instrumentation¶
curl -sv --max-time 300 https://<app>/sleep?s=<n>— observe HTTP code and bodytime curl ...— verify exact duration- ARM API:
az rest GET .../containerApps/aca-diag-batch?api-version=2024-03-01— verify noingress.timeoutARM field exists (non-configurable)
8. Procedure¶
- Deploy Flask app with
/sleep?s=<n>endpoint (gunicorn--timeout 600). - S1–S3: Test sleep values below threshold (10, 60, 230 seconds) — expect 200 OK.
- S4: Test 240-second sleep — observe 504 at exactly 240s.
- S5: Test 241-second sleep — confirm 504 returns at 240s regardless.
- Check ARM API for
ingress.timeoutfield — confirm no user-configurable timeout.
9. Expected signal¶
- S1–S3: HTTP 200 with
{"slept": <n>}body. - S4–S5: HTTP 504 with body
stream timeoutat exactly 240 seconds. - ARM schema: no
timeoutfield in ingress configuration.
10. Results¶
S1 — 10-second sleep¶
S2 — 60-second sleep¶
S3 — 230-second sleep (under boundary)¶
S4 — 240-second sleep (at boundary)¶
curl -sv --max-time 300 "$ACA_URL/sleep?s=240"
→ < HTTP/2 504
→ stream timeout
→ Duration: 240s exactly
S5 — 241-second sleep (over boundary)¶
curl -sv --max-time 300 "$ACA_URL/sleep?s=241"
→ < HTTP/2 504
→ stream timeout
→ Duration: 240s (terminated before backend responded)
Key finding
Container Apps ingress terminates any request that has not sent a response byte within 240 seconds, returning HTTP 504 with body stream timeout. This is a hardcoded platform limit — the ARM schema for ingress has no timeout field.
ARM ingress schema — no timeout field¶
az rest --method get \
--uri ".../containerApps/aca-diag-batch?api-version=2024-03-01" \
--query "properties.configuration.ingress"
# → No "timeout" field in the response
11. Interpretation¶
- Measured: H1 is confirmed. Container Apps ingress terminates connections with no response data at exactly 240 seconds, returning HTTP 504 with body
stream timeout. Measured. - Inferred: H2 (container process continues after connection drop) is consistent with gunicorn's behavior — the worker holds the connection open until the sleep completes, but the ingress has already sent 504 to the client. The gunicorn worker logs the response as completed after the full sleep duration. Inferred (not directly observed — Kudu/exec access not available).
- Not Proven: H3 (streaming prevents timeout) — streaming was not tested. Response chunking should keep the connection alive per HTTP/2 flow control semantics, but this was not verified experimentally.
- Measured: H4 is disproven. There is no
ingress.timeoutARM property. The 240-second limit is a platform constant, not a configurable setting in the 2024-03-01 API. Measured.
12. What this proves¶
- Container Apps ingress times out non-streaming requests at exactly 240 seconds with HTTP 504
stream timeout. Measured. - The timeout is not user-configurable in the current ARM API (no
ingress.timeoutfield). Measured. - 230-second requests complete successfully; 240-second requests do not. The boundary is 240 seconds. Measured.
13. What this does NOT prove¶
- Whether streaming (chunked response with periodic flushes) prevents the timeout was not tested.
- Whether the container process receives a signal (SIGTERM or similar) when the ingress drops the connection was not measured (Kudu exec not available).
- The behavior of Dedicated workload profile was not tested — only Consumption was measured.
- Whether the timeout applies to response headers or only to response body bytes was not differentiated.
14. Support takeaway¶
When a customer reports requests failing after exactly 4 minutes (240 seconds) on Container Apps:
- The root cause is the Container Apps ingress 240-second request timeout. This is a platform constant — it cannot be configured via the ARM API or CLI.
- The 240s timer starts when the request arrives at the ingress and no response bytes have been sent. If the application sends response headers but no body, the timer likely resets per Envoy stream timeout semantics — but this was not verified.
- The response is HTTP 504 with body
stream timeout— this is Envoy's format and distinguishes this from application-level 504s. - Design patterns for long operations:
- Async: accept the request (return 202 + job ID), process in background, expose a polling endpoint.
- Streaming: use
Transfer-Encoding: chunkedwith periodic flushes to keep bytes flowing. - WebSockets: upgrade the connection to WebSocket, which is not subject to the same HTTP timeout.
- Operations that are consistently slower than 240 seconds cannot be exposed as synchronous HTTP endpoints on Container Apps.
15. Reproduction notes¶
ACA_APP="<app-name>"
RG="<resource-group>"
ACA_URL="https://<app>.<env>.azurecontainerapps.io"
# Deploy Flask app with sleep endpoint (gunicorn must have --timeout > 240)
# Dockerfile CMD: gunicorn --bind 0.0.0.0:8000 --timeout 600 app:app
# Test boundary
curl -sv --max-time 300 "$ACA_URL/sleep?s=230" | tail -2 # → 200 OK
curl -sv --max-time 300 "$ACA_URL/sleep?s=240" | tail -2 # → 504 stream timeout
# Verify no timeout ARM field
az rest --method get \
--uri "https://management.azure.com/subscriptions/<sub>/resourceGroups/$RG/providers/Microsoft.App/containerApps/$ACA_APP?api-version=2024-03-01" \
--query "properties.configuration.ingress.timeout"
# → null (field does not exist)