Building the Node endpoint that receives queue push deliveries safely
Infrai's push subscription sends no HMAC signature, so authentication is a shared secret you compare in constant time. A complete Node 22 subscriber with the ack rules.
When you subscribe an HTTPS endpoint to an Infrai queue, your HTTP status code becomes the ack: answer 2xx and the message is deleted, answer anything else and it comes back twice more before dead-lettering. There’s one thing the delivery does not carry, and you need to know it before you write the handler — it isn’t signed. No HMAC header, no timestamp to verify, nothing to compare against a shared secret.
So the first job of the endpoint is proving the caller is Infrai, and the second is being idempotent, because push delivery is at-least-once like everything else on the queue.
What lands on your server
Two identifying headers, Content-Type: application/json, and an envelope:
{
"messages": [
{
"message_id": "qmsg_FxswHtsfY3QeNgKKEWoHkR9g",
"queue": "welcome-mail",
"payload": { "user_id": "u_4417", "template": "welcome" },
"headers": { "X-Trace": "t1" },
"published_at": "2026-07-26T00:50:03.521758Z",
"delivery_count": 1
}
]
}
X-Infrai-Subscription-Id and X-Infrai-Queue tell you which subscription is calling. Both are useful for routing and logging, and neither is a credential — anyone who guesses your URL can send the same pair. The headers object inside the message is data you attached at publish time, not HTTP headers; it rides along in the JSON body and authenticates nothing.
delivery_count is the honest one. On the first attempt it’s 1, and a 2 or a 3 means your last response wasn’t a 2xx.
Authenticate with the URL, in constant time
Since there’s no signature to check, the secret has to be somewhere you control, and the only field the subscription takes besides the queue is the URL. Put a long random segment in the path:
export INFRAI_API_KEY="your_infrai_api_key"
export PUSH_SECRET="$(openssl rand -hex 32)"
curl -sS -X POST "https://api.infrai.cc/v1/queue/create" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"name":"welcome-mail","type":"standard","dlq":"welcome-mail-dlq"}'
curl -sS -X POST "https://api.infrai.cc/v1/queue/push_subscribe/welcome-mail" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"queue\":\"welcome-mail\",\"url\":\"https://worker.example.com/queue/${PUSH_SECRET}\"}"
| Guard | Strength | Notes |
|---|---|---|
Secret path segment, compared with timingSafeEqual | good | 256 bits of entropy; rotate by resubscribing |
Pinning X-Infrai-Subscription-Id to the value you stored | useful | catches misrouted traffic, not forgery |
| IP allowlist | weak here | delivery source addresses aren’t published |
| HMAC signature verification | unavailable | the delivery isn’t signed at all |
| TLS on your endpoint | mandatory | a secret in a URL over plain HTTP is not a secret |
That combination is weaker than a signed webhook — say it out loud so nobody assumes otherwise — but a 32-byte random path over TLS, plus idempotent handling, is a reasonable posture for internal job traffic. Anything carrying money or PII deserves a second check inside your own system: look the entity up by ID rather than trusting the payload’s copy of it.
The subscriber
Dependency-free, so it runs on a plain Node 22 install:
import http from "node:http";
import process from "node:process";
import { timingSafeEqual } from "node:crypto";
const PORT = Number(process.env.PORT ?? 8080);
const SECRET = process.env.PUSH_SECRET;
const SUBSCRIPTION = process.env.PUSH_SUBSCRIPTION_ID ?? "";
if (!SECRET) throw new Error("PUSH_SECRET is required");
const seen = new Map(); // message_id -> timestamp, 1 hour of memory
const SEEN_TTL_MS = 60 * 60 * 1000;
function sameSecret(candidate) {
const a = Buffer.from(candidate ?? "", "utf8");
const b = Buffer.from(SECRET, "utf8");
return a.length === b.length && timingSafeEqual(a, b);
}
function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
let size = 0;
req.on("data", (chunk) => {
size += chunk.length;
if (size > 512 * 1024) { reject(new Error("body too large")); req.destroy(); return; }
chunks.push(chunk);
});
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
req.on("error", reject);
});
}
async function process_message(message) {
const now = Date.now();
for (const [id, at] of seen) if (now - at > SEEN_TTL_MS) seen.delete(id);
if (seen.has(message.message_id)) {
console.log(`duplicate ${message.message_id} (delivery ${message.delivery_count}) ignored`);
return;
}
seen.set(message.message_id, now);
console.log(`sending ${message.payload.template} to ${message.payload.user_id}`);
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, "http://localhost");
const secretFromPath = url.pathname.split("/").filter(Boolean).at(-1);
if (req.method !== "POST" || !url.pathname.startsWith("/queue/") || !sameSecret(secretFromPath)) {
res.writeHead(404).end();
return;
}
if (SUBSCRIPTION && req.headers["x-infrai-subscription-id"] !== SUBSCRIPTION) {
console.warn(`unexpected subscription ${req.headers["x-infrai-subscription-id"]}`);
res.writeHead(404).end();
return;
}
try {
const raw = await readBody(req);
const envelope = JSON.parse(raw);
const messages = Array.isArray(envelope.messages) ? envelope.messages : [];
for (const message of messages) await process_message(message);
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ acked: messages.length }));
} catch (err) {
console.error(`delivery from ${req.headers["x-infrai-queue"]} failed: ${err.message}`);
res.writeHead(500).end();
}
});
server.listen(PORT, () => console.log(`listening on :${PORT}`));
Three decisions in there deserve a sentence each. The unauthenticated path returns 404 rather than 401, so a scanner learns nothing about which URLs exist. The in-memory seen map is fine for a single process and wrong for a fleet — swap it for a unique index or a Redis SET NX when you scale past one instance, because the whole point is that two workers must not both act on qmsg_.... And the handler answers only after the work is done, which is the correct default: a 200 is an irreversible delete.
If you’re already on Express, the same guard is a middleware:
import express from "express";
import process from "node:process";
import { timingSafeEqual } from "node:crypto";
const app = express();
const SECRET = process.env.PUSH_SECRET;
if (!SECRET) throw new Error("PUSH_SECRET is required");
app.use(express.json({ limit: "512kb" }));
function guard(req, res, next) {
const given = Buffer.from(req.params.secret ?? "", "utf8");
const want = Buffer.from(SECRET, "utf8");
if (given.length !== want.length || !timingSafeEqual(given, want)) return res.sendStatus(404);
return next();
}
app.post("/queue/:secret", guard, async (req, res) => {
const messages = Array.isArray(req.body?.messages) ? req.body.messages : [];
try {
for (const message of messages) {
console.log(`${req.get("X-Infrai-Queue")} -> ${message.message_id} (delivery ${message.delivery_count})`);
}
res.status(200).json({ acked: messages.length });
} catch (err) {
console.error(err.message);
res.sendStatus(500);
}
});
app.listen(Number(process.env.PORT ?? 8080));
Answer fast, and mean it
| Your response | What the dispatcher does |
|---|---|
| 200–299 | treats the message as acked and deletes it |
| 4xx | retries — it can’t tell “invalid” from “unavailable” |
| 5xx | retries: three attempts about six seconds apart, then the DLQ |
| No response / hang | the attempt is lost; the retry budget still burns |
Because every non-2xx costs the same three attempts, a payload your handler can never process should be accepted with a 200 and quarantined on your side. Sending 400 just moves it to the dead-letter queue six seconds later without telling anyone why.
Keep the handler well under a second of work if you can. We didn’t measure the dispatcher’s read timeout, so treat a slow handler as an unknown rather than a safe bet — if a job takes minutes, write it to your own store, answer 200, and process it with a worker.
Prove the loop end to end
Fire a synthetic delivery at your local server first:
curl -sS -X POST "http://localhost:8080/queue/${PUSH_SECRET}" \
-H "Content-Type: application/json" \
-H "X-Infrai-Queue: welcome-mail" \
-H "X-Infrai-Subscription-Id: sub_local_test" \
-d '{"messages":[{"message_id":"qmsg_local_1","queue":"welcome-mail","payload":{"user_id":"u_4417","template":"welcome"},"headers":null,"published_at":"2026-07-26T00:50:03.521758Z","delivery_count":1}]}'
Then publish for real and watch it arrive — in our testing the POST landed about five seconds after the publish returned:
curl -sS -X POST "https://api.infrai.cc/v1/queue/publish" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"queue":"welcome-mail","body":{"user_id":"u_4417","template":"welcome"}}'
curl -sS "https://api.infrai.cc/v1/queue/stats/welcome-mail" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
A drained queue answers with every counter at zero. If dlq_count moved instead, your endpoint returned something outside the 2xx range three times — check the secret first, since a 404 from the guard looks exactly like a missing route. Acking a message that’s no longer leased is a different error, QUEUE_MESSAGE_NOT_IN_FLIGHT, and it only applies to the polling path.
Cost, caveats, alternatives
The subscription itself is free; only publishing is metered, at $0.00002 per message, verified 2026-07-26. Consume, ack, stats and dead-letter reads are free and rate-limited too, so a push architecture costs exactly what the publishes cost:
curl -sS "https://api.infrai.cc/v1/discovery" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
| jq '.capabilities[] | select(.id == "queue.push_subscribe") | .billing'
Rates on this surface trend down and new accounts carry $2 of credit, so the live figure is the one to quote internally.
The caveats are concentrated in the subscription itself: no signature, no documented unsubscribe route, and a second subscribe call returns a new subscription_id while deliveries keep going to the endpoint registered first. Concurrency is fixed at 10 and retries at 3. If signed deliveries with configurable retry are a requirement rather than a preference, QStash publishes a verification recipe and per-message retry settings, and you’d be better off there. SQS with a Lambda event-source mapping avoids public endpoints altogether by pulling on your behalf inside AWS’s network.
What you get here instead is one credential for the whole job: the queue that delivers the message, the storage the handler writes to, the email it sends, and the error tracking that catches it when the handler throws.