Put a Queue Between Hyperframes and Your Traffic Spike
It's 3 AM. Your phone buzzes. Video rendering is timing out across the board. You check logs: 127 concurrent render jobs, all started in the last ninety seconds. Your rendering tier was sized for four. Every new request makes it worse. You've just learned that moving an HTTP endpoint to the edge does not create rendering capacity—it just delivers overload faster.
Hyperframes describes its contract as "Write HTML. Render video. Built for agents." As an external architect evaluating that contract, I would keep HTML submission close to users and video rendering far away from the request lifecycle. The developer pain point is burst admission: a hundred requests arrive together, but the rendering tier can safely execute only four jobs. Without explicit backpressure, unconstrained admission becomes unconstrained resource acquisition.
This design uses Hono for admission, Cloudflare Queues for buffering, and KV for completed-result lookup. It treats Hyperframes as the rendering engine behind a separately deployed HTTP adapter, not as a package that magically runs inside a Worker.
The boundary that matters
The edge validates a small JSON envelope and returns 202 only after the queue accepts it. A queue consumer calls a private rendering adapter. That adapter invokes Hyperframes in an isolated rendering environment and returns an artifact URL.
The adapter interface below is an integration contract, not a claimed Hyperframes API:
POST /render accepts { id, html }, honors Idempotency-Key, and returns { url } after durable artifact storage. It must reuse completed results for repeated IDs and serialize concurrent requests for the same ID.
This matters because queue delivery is at least once. A renderer can finish successfully while its response disappears. Retrying without durable idempotency renders the same video twice.
A deployable edge boundary
The following single setup block creates both required files. Install a current Node.js LTS first. Replace the example hostname, zone, KV ID, and renderer URL before deploying.
npm init -y
npm install hono
npm install -D wrangler typescript @cloudflare/workers-types
mkdir -p src
cat > src/index.ts <<'TS'
import { Hono } from 'hono'
import { bodyLimit } from 'hono/body-limit'
type Job = { id: string; html: string }
type Env = {
RESULTS: KVNamespace
RENDERS: Queue<Job>
API_TOKEN: string
RENDER_TOKEN: string
RENDER_URL: string
}
const app = new Hono<{ Bindings: Env }>()
app.use('*', async (c, next) => {
if (c.req.header('Authorization') !== `Bearer ${c.env.API_TOKEN}`)
return c.json({ error: 'unauthorized' }, 401)
await next()
})
app.use('*', bodyLimit({
maxSize: 65536,
onError: c => c.json({ error: 'payload_too_large' }, 413)
}))
app.post('/jobs', async c => {
const input = await c.req.json().catch(() => null)
if (!input || typeof input.html !== 'string' ||
!input.html.trim() || input.html.length > 16000)
return c.json({ error: 'invalid_html' }, 400)
const id = crypto.randomUUID()
await c.env.RENDERS.send({ id, html: input.html })
return c.json({ id, status: 'accepted' }, 202)
})
app.get('/jobs/:id', async c => {
const id = c.req.param('id')
if (!/^[0-9a-f-]{36}$/.test(id))
return c.json({ error: 'invalid_id' }, 400)
const result = await c.env.RESULTS.get(id, 'json')
return result
? c.json({ id, status: 'complete', result })
: c.json({ id, status: 'not_available' }, 404)
})
app.onError((error, c) => {
console.error('request_failed', error.message)
return c.json({ error: 'internal_error' }, 500)
})
export default {
fetch: app.fetch,
async queue(batch: MessageBatch<Job>, env: Env) {
for (const message of batch.messages) {
const response = await fetch(env.RENDER_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.RENDER_TOKEN}`,
'Content-Type': 'application/json',
'Idempotency-Key': message.body.id
},
body: JSON.stringify(message.body),
signal: AbortSignal.timeout(55000)
})
if (!response.ok) throw new Error(`renderer_${response.status}`)
const result = await response.json() as { url?: unknown }
if (typeof result.url !== 'string' ||
!result.url.startsWith('https://'))
throw new Error('invalid_renderer_result')
await env.RESULTS.put(message.body.id, JSON.stringify(result), {
expirationTtl: 86400
})
message.ack()
}
}
}
TS
cat > wrangler.jsonc <<'JSON'
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "hyperframes-admission",
"main": "src/index.ts",
"compatibility_date": "2026-10-01",
"workers_dev": false,
"routes": [
{ "pattern": "video.example.com/*", "zone_name": "example.com" }
],
"vars": {
"RENDER_URL": "https://renderer.example.com/render"
},
"kv_namespaces": [
{ "binding": "RESULTS", "id": "REPLACE_WITH_KV_NAMESPACE_ID" }
],
"queues": {
"producers": [
{ "binding": "RENDERS", "queue": "video-renders" }
],
"consumers": [
{
"queue": "video-renders",
"max_batch_size": 1,
"max_concurrency": 4,
"max_retries": 3,
"dead_letter_queue": "video-renders-dead"
}
]
}
}
JSON
npx wrangler kv namespace create RESULTS
npx wrangler queues create video-renders
npx wrangler queues create video-renders-dead
npx wrangler secret put API_TOKEN
npx wrangler secret put RENDER_TOKEN
# Insert the returned KV ID and replace example domains before deployment.
npx wrangler deploy
Secrets belong in Wrangler's secret store, not plaintext configuration. The declared TypeScript bindings document their names.
Where tutorials stop working
A familiar serverless Postgres incident starts with harmless per-request connection creation. Traffic spikes, instance counts multiply, and the database exhausts its pool. Rendering has the same shape. Four consumer invocations bound dispatch concurrency; the adapter must enforce its own global execution limit too.
Do not import browser automation or Node compatibility shims into the ingress bundle. That shifts heavyweight dependencies into startup and can push the Worker beyond bundle limits without adding rendering capacity.
The 55-second timeout deliberately restricts this synchronous adapter to short renders. Longer jobs need a submit-and-poll adapter, not an ever-larger timeout. Dead-letter depth and oldest-message age need alerts; bounded retries do not constitute recovery.
KV is eventually consistent. A missing result means unavailable, not failed, and completed results expire after one day. Client retries of POST /jobs also create distinct jobs; production billing requires a durable client-idempotency layer.
Finally, HTML is executable input. Disable arbitrary renderer network access, isolate browser processes, and restrict artifact destinations. Validation at the edge limits payloads; it does not make submitted HTML safe.
The hardest trade-off
This pattern protects the rendering tier but creates a new problem: clients now poll for results. That polling becomes its own traffic spike. You can add webhook callbacks to the adapter, but now you need webhook retry logic, endpoint verification, and failure tracking. The synchronous adapter with its 55-second timeout avoids all of that, but it means longer renders cannot use this pattern at all.
Which failure mode would you rather debug at 3 AM: webhook delivery gone silent, or polling clients hammering your edge? What's your production rendering pattern, and where did it break under load?
Top comments (0)