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}\"}"
GuardStrengthNotes
Secret path segment, compared with timingSafeEqualgood256 bits of entropy; rotate by resubscribing
Pinning X-Infrai-Subscription-Id to the value you storedusefulcatches misrouted traffic, not forgery
IP allowlistweak heredelivery source addresses aren’t published
HMAC signature verificationunavailablethe delivery isn’t signed at all
TLS on your endpointmandatorya 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 responseWhat the dispatcher does
200–299treats the message as acked and deletes it
4xxretries — it can’t tell “invalid” from “unavailable”
5xxretries: three attempts about six seconds apart, then the DLQ
No response / hangthe 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.

References

Browse more queue developer guides