Protocol Specification

PIMP — Polling Inverse
Messaging Protocol

A platform-agnostic protocol for streaming binary or text data over stateless serverless infrastructure via chunked polling.

STATUS: DRAFT  ·  VERSION: 0.5.0  ·  ENCODING: Hybrid (Base64/Identity)
Table of Contents
  1. Conventions & Terminology
  2. Motivation
  3. Encoding Strategies
  4. Metadata Chunk (Index 0)
  5. Protocol Envelope
  6. Job Creation & Handoff
  7. Write API (Sender → Buffer)
  8. Sender Behavior
  9. Consumer Behavior & Polling API (Buffer → Consumer)
  10. Job Lifecycle & States
  11. Error Handling
  12. Configuration & Defaults
  13. Security Considerations
  14. End-to-End Examples

Conventions & Terminology

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 / RFC 8174.

TermDefinition
JobA single logical stream of data identified by a unique jobId (UUIDv4).
ChunkAn individually addressable unit of data within a job, identified by a zero-based index.
SenderThe process that produces chunks and writes them to the buffer.
ConsumerThe process that polls for and reads chunks from the buffer.
BufferThe intermediary store (e.g. KV, database) that holds chunks between sender and consumer.

Motivation

Serverless platforms often restrict response bodies to valid UTF-8 text strings. PIMP enables binary delivery over these transports using chunked polling, with an optimized "Text Pass-through" mode to avoid unnecessary encoding overhead for text-based streams.

Encoding Strategies

To balance platform safety with performance, PIMP supports two encoding modes defined at the job level via contentEncoding in the Metadata Chunk.

1. Base64 Mode (Binary-Safe)

Used for non-textual data (images, PDFs, protobufs). Each data chunk value is a standard Base64-encoded string (RFC 4648 §4). The consumer MUST decode each chunk value before appending to the output buffer.

2. Identity Mode (Text Pass-through)

Used for textual data (JSON, Markdown, CSV). Each data chunk value is a raw UTF-8 string. No decoding is required. This avoids the ~33% bandwidth overhead and the computational cost of double-parsing on the consumer.

If a chunk value contains characters outside the valid UTF-8 range, the sender MUST fall back to Base64 mode for the entire job. Partial mode switching within a job is not permitted.

Recommendation: Senders SHOULD use Identity mode whenever contentType begins with text/, application/json, or any application/*+json subtype.

Metadata Chunk (Index 0)

The first chunk (index 0) is reserved for job configuration. It MUST be written to the buffer immediately upon job start. Its value is a JSON object with the following fields:

FieldTypeRequiredDescription
contentType string REQUIRED MIME type of the original payload, e.g. "application/json", "image/png".
contentEncoding string REQUIRED "identity" or "base64". Determines how all subsequent chunk values are encoded.
compression string OPTIONAL Compression applied to the original payload before chunking. "none" (default) or "gzip". When set, contentLength refers to the compressed size.
contentLength number OPTIONAL Total byte length of the reassembled payload (after decoding, before decompression). When compression is "gzip", this is the compressed byte length.
meta object OPTIONAL Arbitrary application-specific context. Implementations MUST NOT interpret this field.
// Example Metadata Chunk (index 0)
{
  "contentType": "application/json",
  "contentEncoding": "identity",
  "compression": "none",
  "contentLength": 48210,
  "meta": { "requestId": "abc-123" }
}

Protocol Envelope

Every chunk stored in the buffer MUST be wrapped in the following envelope structure:

{
  "jobId":     string,  // UUIDv4 — unique per job
  "index":     number,  // 0-based sequential chunk index
  "value":     string,  // chunk payload (metadata JSON for 0, encoded data for 1+)
  "done":      boolean, // true on the final chunk only
  "error":     string | null, // non-null if job failed — see §9
  "createdAt": number   // Unix epoch milliseconds
}

Field Rules

Job Creation & Handoff

The consumer is responsible for creating the job. It generates a jobId (UUIDv4) and passes it — along with the buffer's write URL — to the sender as part of the trigger request.

Trigger Request

The consumer initiates work by calling the sender's endpoint with at minimum:

{
  "jobId":    string,  // generated by the consumer
  "writeUrl": string,  // buffer endpoint the sender will POST chunks to
  "writeKey": string,  // short-lived auth token scoped to this job
  "payload":  object   // application-specific input (e.g. LLM prompt)
}

The sender MUST respond immediately (HTTP 202 Accepted) to acknowledge the job, then begin producing chunks asynchronously. The consumer does not wait for the sender to finish — it begins polling the buffer for chunks.

Sequence

  Consumer                 Sender                  Buffer
     │                        │                        │
     │── POST trigger ───────▸│                        │
     │◂── 202 Accepted ──────│                        │
     │                        │── POST chunk 0 ──────▸│
     │── GET /pimp/{id} ────────────────────────────▸│
     │◂── { chunks: [0] } ──────────────────────────│
     │                        │── POST chunk 1..N ──▸│
     │── GET /pimp/{id} ────────────────────────────▸│
     │◂── { chunks: [1..N] } ───────────────────────│
     │                        │── POST final (done) ─▸│
     │── GET /pimp/{id} ────────────────────────────▸│
     │◂── { chunks: [done] } ───────────────────────│
     ▼                        ▼                        ▼
    

Write API (Sender → Buffer)

The sender pushes chunks to the buffer store via HTTP POST. This is the counterpart to the read/poll API in §9.

Write Request

POST /pimp/{jobId}/chunks
Authorization: Bearer {writeKey}
Content-Type: application/json

{
  "chunks": [
    /* one or more envelope objects (§5) */
  ]
}

The buffer store MUST validate that:

Write Response

// Success
201 Created
{ "written": 3 }  // number of new chunks accepted

// Duplicate chunks (already stored) — not an error
200 OK
{ "written": 0, "duplicates": 3 }

// Auth failure
401 Unauthorized

Idempotency

If the sender retries a write (e.g. after a network timeout), the buffer MUST accept the request without error. Chunks with an index that already exists for the job MUST be silently ignored. This makes write operations safe to retry.

Sender Behavior

Immediate Metadata Flush

The Metadata Chunk (index 0) MUST NOT be buffered. It MUST be written to the buffer immediately upon job creation, before any data chunks, to allow the consumer to negotiate decoding strategies.

Data Batching

Subsequent chunks (index ≥ 1) are buffered locally and flushed to the buffer store when either condition is met:

End of Stream

After the last data segment, the sender MUST write a final chunk with done: true. If the final chunk carries data, done is set on that chunk. Otherwise, an empty sentinel chunk (value: "", done: true) MUST be appended.

Max Chunk Size

Individual chunk value strings SHOULD NOT exceed 256 KiB (262,144 bytes) to remain within common platform limits. Implementations MAY support a configurable ceiling.

Consumer Behavior & Polling API

Poll Request

The consumer requests chunks by providing jobId and the next expected index:

GET /pimp/{jobId}?from={index}

The buffer store MUST return all available chunks with index >= from, ordered by index. If no new chunks are available, the store MUST return an empty array with HTTP 200.

Poll Response

{
  "chunks": [ /* array of envelope objects */ ],
  "nextIndex": number  // = max(returned index) + 1
}

Polling Cadence

The consumer begins polling at consumer.pollIntervalMs. Implementations SHOULD use exponential backoff when receiving consecutive empty responses, up to a maximum of consumer.maxPollIntervalMs. Upon receiving data, the interval MUST reset to consumer.pollIntervalMs.

Completion

When the consumer receives a chunk with done: true, it MUST stop polling. The reassembled payload is the concatenation of all value fields from index 1 through the final chunk, decoded according to contentEncoding, then decompressed per compression.

Job Lifecycle & States

A job transitions through the following states:

  ┌──────────┐    index 0 written     ┌──────────┐
  │ CREATED  │ ──────────────────▸ │ ACTIVE   │
  └──────────┘                        └────┬─────┘
                                           │
                          ┌────────────────┬┴────────────────┐
                          ▼                ▼                  ▼
                   ┌────────────┐   ┌────────────┐   ┌────────────┐
                   │ COMPLETED  │   │ FAILED     │   │ EXPIRED    │
                   └────────────┘   └────────────┘   └────────────┘
                   done: true       error != null    ttl exceeded
    
StateTriggerConsumer Action
CREATEDJob ID allocated, no chunks yet.Poll; expect metadata shortly.
ACTIVEMetadata chunk written; data streaming.Poll and reassemble chunks.
COMPLETEDChunk with done: true received.Stop polling; finalize payload.
FAILEDChunk with non-null error received.Stop polling; surface error.
EXPIREDjob.ttlMs elapsed since creation.Stop polling; treat as timeout.
Important: Buffer implementations MUST delete all chunks for a job once job.ttlMs has elapsed, regardless of completion state.

Error Handling

Sender-Side Failure

If the sender encounters an unrecoverable error, it MUST write a final chunk with done: true and error set to a human-readable message. The value field MAY be empty.

// Error sentinel chunk
{
  "jobId": "550e8400-...",
  "index": 4,
  "value": "",
  "done": true,
  "error": "upstream timeout after 30s",
  "createdAt": 1713600000000
}

Consumer-Side Resilience

Missing Chunks

If the consumer detects a gap (e.g. receives index 5 without having index 4), it MUST NOT advance reassembly past the gap. It SHOULD continue polling with from set to the missing index.

Configuration & Defaults

Implementations MUST provide these defaults to ensure out-of-the-box functionality. All timing values are integers in milliseconds.

PropertyTypeDefaultDescription
job.ttlMs number 300000 Maximum job lifetime. Buffer MUST evict after expiry.
sender.maxChunksPerBatch number 10 Max number of chunks buffered locally before flushing to the store.
sender.flushIntervalMs number 250 Max milliseconds to wait before flushing a partial batch.
consumer.pollIntervalMs number 500 Initial poll frequency in milliseconds.
consumer.maxPollIntervalMs number 5000 Upper bound for exponential backoff.
consumer.stallTimeoutMs number 30000 Max time without new chunks before the consumer treats the job as stalled.

Security Considerations

Write Authentication

The buffer's write endpoint MUST require a writeKey that is scoped to a single jobId. The consumer generates this key at job creation time and passes it to the sender in the trigger request. The key SHOULD be a cryptographically random token (≥ 32 bytes) or a signed JWT with a jobId claim and a short expiry (≤ job.ttlMs).

Read Access

The poll endpoint MAY be unauthenticated if jobId values are sufficiently unguessable (UUIDv4 provides 122 bits of entropy). For higher-security deployments, implementations SHOULD require a separate read token or tie access to the originating session.

Transport

All endpoints (trigger, write, poll) MUST be served over HTTPS. Implementations MUST NOT transmit writeKey values over unencrypted channels.

Payload Size Limits

Buffer implementations SHOULD enforce a maximum total job size (e.g. 50 MiB) and a maximum chunk count to prevent abuse. Senders exceeding these limits SHOULD receive a 413 Payload Too Large response.

End-to-End Examples

Example A — Identity / JSON Stream

Sender streams a JSON array of records to a consumer.

// Chunk 0 — Metadata (written immediately)
{ "jobId": "a1b2c3...", "index": 0,
  "value": "{\"contentType\":\"application/json\",\"contentEncoding\":\"identity\",\"compression\":\"none\"}",
  "done": false, "error": null, "createdAt": 1713600000000 }

// Chunk 1 — Data
{ "jobId": "a1b2c3...", "index": 1,
  "value": "[{\"id\":1,\"name\":\"Alice\"},{\"id\":2,\"name\":",
  "done": false, "error": null, "createdAt": 1713600000100 }

// Chunk 2 — Final data
{ "jobId": "a1b2c3...", "index": 2,
  "value": "\"Bob\"}]",
  "done": true, "error": null, "createdAt": 1713600000200 }

// Consumer reassembles: value[1] + value[2]
// → [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]

Example B — Base64 / Binary Image

Sender streams a small PNG to a consumer.

// Chunk 0 — Metadata
{ "jobId": "d4e5f6...", "index": 0,
  "value": "{\"contentType\":\"image/png\",\"contentEncoding\":\"base64\",\"compression\":\"none\",\"contentLength\":1024}",
  "done": false, "error": null, "createdAt": 1713600001000 }

// Chunk 1 — Base64-encoded segment
{ "jobId": "d4e5f6...", "index": 1,
  "value": "iVBORw0KGgoAAAANSUhEUgAA...",
  "done": false, "error": null, "createdAt": 1713600001050 }

// Chunk 2 — Final segment
{ "jobId": "d4e5f6...", "index": 2,
  "value": "AAAElFTkSuQmCC",
  "done": true, "error": null, "createdAt": 1713600001100 }

// Consumer reassembles: base64Decode(value[1] + value[2])
// → <binary PNG bytes>
Changelog — v0.5.0
  • Added §6 Job Creation & Handoff — trigger request schema, sequence diagram, consumer-owns-jobId pattern.
  • Added §7 Write API — POST endpoint for sender → buffer chunk writes, idempotency guarantees.
  • Added §13 Security Considerations — writeKey auth, read access, HTTPS, payload size limits.
  • Reordered sections to reflect the full request lifecycle: create → write → poll → complete.

Changelog — v0.4.0
  • Added §1 Conventions & Terminology (RFC 2119/8174 reference).
  • Added Protocol Envelope with wire-level chunk schema.
  • Added Consumer Behavior & Polling API with request/response contract.
  • Added Job Lifecycle & States (CREATED → ACTIVE → COMPLETED | FAILED | EXPIRED).
  • Added Error Handling (sender errors, consumer resilience, gap handling).
  • Added End-to-End Examples (identity JSON, base64 binary).
  • Renamed sender.bufferSizesender.maxChunksPerBatch.
  • Normalized all timing config to integer milliseconds (*Ms suffix).
  • Clarified contentLength semantics relative to compression.
  • Expanded Metadata Chunk to a full field-by-field table with types and requiredness.
  • Added consumer.maxPollIntervalMs and consumer.stallTimeoutMs config keys.

PIMP v0.5.0 — DRAFT — Polling Inverse Messaging Protocol