Jobs API
Any request that does not hit a job endpoint is routed to an upstream and queued, unless it skips the queue. The job endpoints live under jobsPath, /__jobs by default. There is no authentication on any of them; see security.
| Method | Path | Does |
|---|---|---|
| any | any other path | Queue the request and return a job ID |
GET | /__jobs/{id} | Job status |
GET | /__jobs/{id}/content | Replay the stored upstream response |
DELETE | /__jobs/{id} | Cancel and delete the job |
Telling proxq from the upstream
proxq sets X-Proxq-Source: proxq on the answers it writes itself on the request path: 202, 400, 500, 502, 307, and on every 404 from the job endpoints. A response replayed from /content never carries it, even when the upstream returned 404 or 500.
On /content, a 404 with X-Proxq-Source means the job is not done or does not exist, and any response without it is the upstream speaking.
The 200 answers from the status and cancel endpoints carry no X-Proxq-Source, and neither do the 500 answers those endpoints write when Redis fails.
proxq’s own errors share one JSON shape:
{
"code": "NOT_FOUND",
"message": "Not found"
}
The codes are BAD_REQUEST, NOT_FOUND, INTERNAL_SERVER_ERROR and BAD_GATEWAY. Every response also carries an X-Request-Id header, taken from the request when the client sent one and generated otherwise.
Submit a request
Send the request you want proxied, with any method, headers and body:
POST /api/heavy-computation HTTP/1.1
Content-Type: application/json
Authorization: Bearer upstream-token
{"data": "lots of it"}
HTTP/1.1 202 Accepted
Content-Type: application/json
X-Proxq-Source: proxq
{
"jobId": "550e8400-e29b-41d4-a716-446655440000"
}
The job ID is a random UUID. The stored request keeps the method, the URL after prefix stripping, every header, the client IP and scheme, and the body up to the upstream’s maxBodySize.
| Answer | When |
|---|---|
202 Accepted | Queued |
400 Bad Request | X-Proxq-Timeout is not a valid duration |
502 Bad Gateway | No upstream prefix matches the path |
500 Internal Server Error | The body could not be read, or Redis refused the job |
Per-request timeout
X-Proxq-Timeout overrides the upstream’s timeout for one request. It takes a Go duration such as 30s or 10m:
curl -s -X POST localhost:8080/api/slow \
-H 'X-Proxq-Timeout: 15m' \
-d '{}'
The header is also forwarded to the upstream with the rest.
Get the job status
GET /__jobs/550e8400-e29b-41d4-a716-446655440000 HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"completedAt": "2026-01-01T00:00:00Z"
}
A failed job carries the last error:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"error": "forward request: do request: ..."
}
| Status | Meaning | asynq states |
|---|---|---|
queued | Waiting for a worker | pending, scheduled, aggregating |
running | A worker has it, or a retry is pending | active, retry |
completed | Done, response stored, whatever status the upstream returned | completed |
failed | Forwarding broke and the retries ran out | archived |
completedAt appears once the job has completed. error holds the last error and can be present on a running job that is waiting for a retry. An unknown ID returns 404 with X-Proxq-Source.
Get the job content
This is the payoff. proxq replays the upstream response exactly: status code, headers and body, as if you had called the upstream yourself.
GET /__jobs/550e8400-e29b-41d4-a716-446655440000/content HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
X-Custom-Header: from-upstream
{"result": "done"}
The content can be fetched as many times as you like until the job expires after taskRetention. When caching is on, the replay also carries X-Cache-Status; see caching.
A job that is not completed, including a failed one, and an unknown ID both return 404 with X-Proxq-Source. Check the status to tell them apart.
Cancel a job
DELETE /__jobs/550e8400-e29b-41d4-a716-446655440000 HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "cancelled"
}
proxq first asks a worker running the job to stop, then deletes the job from Redis. The stored response goes with it, and there is no undo. An unknown ID returns 404 with X-Proxq-Source. There is no ownership check: anyone who can reach proxq and knows a job ID can cancel it.
asynq cannot delete a job while a worker still holds it. Cancelling a running job can therefore return 500 after the stop signal went out; the stopped attempt counts as a failure, so the job is retried or ends failed. Send the DELETE again once the status is no longer running.
A polling loop
JOB_ID=$(
curl -s -X POST localhost:8080/api/report -d '{}' |
jq -r .jobId
)
while true; do
STATUS=$(curl -s "localhost:8080/__jobs/$JOB_ID" | jq -r .status)
case "$STATUS" in
completed)
curl -s "localhost:8080/__jobs/$JOB_ID/content"
break
;;
failed)
echo "job failed" >&2
break
;;
*)
sleep 2
;;
esac
done
The OpenAI Go client does the same loop inside an http.RoundTripper.
Headers
Forwarded to the upstream
Queued requests forward every original header, except the hop-by-hop headers Connection, Keep-Alive, Proxy-Authenticate, Proxy-Authorization, TE, Trailers, Transfer-Encoding and Upgrade. proxq sets these:
| Header | Value |
|---|---|
X-Forwarded-For | Client IP |
X-Real-IP | Client IP, same value |
X-Forwarded-Proto | An incoming X-Forwarded-Proto when present, else http (proxq itself serves plain HTTP) |
The client IP is the first address of an incoming X-Forwarded-For, else an incoming X-Real-IP, else the connection’s address. proxq replaces an incoming X-Forwarded-For chain with that one address, and a client can set it to anything; see security.
The Host header of a queued request is the upstream’s host. A direct-proxied request keeps the client’s Host; see routing.
Set by proxq
| Header | Value | When |
|---|---|---|
X-Proxq-Source | proxq | Answers proxq writes itself, as described above |
X-Cache-Status | HIT or MISS | On replayed responses when caching is on |
X-Request-Id | request ID | Every response |
Read from the client
| Header | Effect |
|---|---|
X-Proxq-Timeout | Overrides the upstream timeout for this request. Invalid value: 400 |