Produce API¶
The produce routes take the request syntax of Confluent's REST API v3, with the namespace in place of the cluster. They reply once every in-sync replica has acknowledged the record, so a client that only reaches port 443 can publish as well as consume.
| Route | Body | Success |
|---|---|---|
POST /api/v3/{namespace}/topics/{topic}/records |
One record | 200, the record's topic, partition, offset and timestamp |
POST /api/v3/{namespace}/topics/{topic}/records:batch |
{"entries": [...]}, 1 to 500 records |
207, successes and failures |
Both routes take the subject and authorization headers (Getting subject and authorization). The namespace in the path must be one the user holds, such as the default one, $NAMESPACE, and the topic one the namespace holds, named without the namespace; the management routes create topics.
The SDK's Client.produce and Client.produce_batch call these routes. ConsumerRESTDemo.ipynb produces records with plain HTTP requests and then with the SDK.
Examples¶
# Produce one record
curl -X POST "https://diaspora-web-service.qpp943wkvr7b2.us-east-1.cs.amazonlightsail.com/api/v3/$NAMESPACE/topics/my-topic/records" \
-H "subject: $SUBJECT" \
-H "authorization: $AUTHORIZATION" \
-H "Content-Type: application/json" \
-d '{"key": {"type": "STRING", "data": "job-42"},
"value": {"type": "JSON", "data": {"step": 7, "loss": 0.31}},
"headers": [{"name": "trace", "value": "AAE="}]}'
{"topic_name": "my-topic", "partition_id": 0, "offset": 12, "timestamp": "2026-10-26T15:04:05.123Z"}
# Produce a batch of records
curl -X POST "https://diaspora-web-service.qpp943wkvr7b2.us-east-1.cs.amazonlightsail.com/api/v3/$NAMESPACE/topics/my-topic/records:batch" \
-H "subject: $SUBJECT" \
-H "authorization: $AUTHORIZATION" \
-H "Content-Type: application/json" \
-d '{"entries": [
{"id": "1", "value": {"type": "JSON", "data": {"step": 8}}},
{"id": "2", "value": {"type": "AVRO", "data": {"step": 9}}}]}'
{
"successes": [
{"id": "1", "topic_name": "my-topic", "partition_id": 0, "offset": 13, "timestamp": "2026-10-26T15:04:05.456Z"}
],
"failures": [
{"id": "2", "error_code": 42206, "message": "value type AVRO needs a schema registry; Octopus runs no schema registry, so use BINARY, JSON or STRING"}
]
}
Records¶
A produce request needs Content-Type: application/json; a charset parameter, if present, must be UTF-8. Any other content type is refused with 415.
A record has the optional fields key, value, headers, partition_id and timestamp, and needs a key or a value:
keyandvalueare{"type": ..., "data": ...}.BINARYdata is base64 and is written decoded;JSONdata is any JSON value, written compactly as UTF-8;STRINGdata is a string, written as UTF-8. Absent or nulldatawrites a null key or value. Octopus runs no schema registry, so theAVRO,PROTOBUFandJSONSCHEMAtypes, the schema fields (schema,schema_id,schema_version,subject,subject_name_strategy) and a key or value without atypeare refused with 422.- A record whose key and value are both null or absent is refused with 400: kafka-python, which the service sends with, cannot send one.
headersis a list of{"name": string, "value": base64 string or null}; a null value is written as an empty one, because kafka-python cannot send a null header value.partition_idmay only be 0: every topic has one partition, and any other partition is not found (404).timestampis ISO-8601 with a UTC offset (for example2026-10-26T15:04:05.123Z) and becomes the record's timestamp; without it, the record takes the time it is produced. The reply gives the record's timestamp in UTC.
Batches¶
The single-record route takes exactly one record: an empty body is refused with 422 and a body that holds more than one JSON value with 400. The batch route takes 1 to 500 entries, each a record with an id of 1 to 80 letters, digits, dashes or underscores that is distinct within the batch. It answers 207 once every entry has been acknowledged or has failed, within the 30 s, with each entry in successes or failures. An empty body, missing or empty entries, more than 500 entries, or a missing, invalid or repeated id refuses the whole batch with 422.
Limits¶
A record holds at most 8 MiB (8,388,608 bytes), including about 100 bytes of Kafka's per-record overhead: its key, value and header names and values, once decoded, must fit in the rest. A request body holds at most 16 MiB, and so do a batch's records once encoded: JSON data is written compactly, but a number is written in full (1e15 as 1000000000000000.0), so a batch can grow past its body and is then refused with 413.
A body whose JSON would take too much memory to parse is refused with 413 before it is parsed. The service estimates the cost from the body's bytes and refuses an estimate over 192 MiB. The estimate counts 4 MiB for any body; w bytes for each byte of the body, where w is 1, 2 or 4 by the widest character the body holds or escapes (\u4e2d counts 2, \ud83d\ude00 counts 4); 2w + 2 bytes for each byte of JSON it may write, which can be longer than the body by up to 14 bytes for each number with an exponent and 3 for each decimal point; and 160 bytes for each { or [, 32 for each ", 40 for each , and 48 for each :. So a body of long ASCII strings or base64 passes up to the 16 MiB, a JSON array of four-digit numbers ([1000,1000,...]) up to about 14.5 MiB, a string of emoji up to about 13.4 MiB, an object with unique keys ({"1":1,"2":1,...}) up to about 9.9 MiB, an array of single digits or of numbers like 1e15 up to about 7.5 MiB, and one of empty objects or arrays ([{},{},...]) up to about 2.6 MiB.
The service holds at most 48 MiB of produce bodies and records at once, and at most 16 MiB for any one user (subject). A request holds the bytes of its body that have arrived, whatever its Content-Length declares, and once parsed the size of its records if that is larger. Once its body has arrived, a request takes one of the 8 places the service has for parsing and sending; a request still sending its body takes none. A user may have at most 2 requests in flight, at any stage. A request past a limit is refused with 429 and Retry-After: 1, as soon as it passes it: when it arrives, while its body arrives, or once it is read or parsed. When it arrives, a request is also refused before its body is sent if every place is taken or its Content-Length is more than the bytes free, for the service or for its user; nothing is held for it until its bytes arrive. A request's body must arrive within 20 s of its admission, which needs about 7 Mbit/s for a body of 16 MiB; a slower body is refused with 408. The service closes the connection after a 408, a 413 or a 429.
Acknowledgement¶
The records' 30 s start once the body is read and parsed, and the records of a request share them to be acknowledged. A record not sent by then fails with 50003 and is not written; a record sent but not acknowledged by then fails with 50003 and may still be written. A 200 or a success in a 207 does not rule out a second copy either: the producer is not idempotent and retries a failed write within the 30 s, so a write that reached the cluster but whose acknowledgement was lost can be written twice. Consumers that need each record once should carry an id in the record and drop repeats.
Every topic has replication factor 2 on the cluster's two brokers, and the cluster's min.insync.replicas is 2, so a write needs both brokers to acknowledge it. While one broker restarts, for a cluster update or after a failure, the cluster refuses writes as NOT_ENOUGH_REPLICAS. The producer retries such a write until the record's 30 s are spent, and the record then fails with 50003 and the message "The cluster did not acknowledge the record in time; it may still be written": a single-record request answers 500, and a batch answers 207 with the entry in failures. Clients should retry such records with backoff, for example from 1 s doubling to 30 s, for at least as long as a broker takes to restart. Since the record may still be written, a retry can write it twice.
Errors¶
While the service cannot reach the Kafka cluster, it answers 503 for 5 s before it tries again, with Retry-After set to the seconds left. A topic that the namespace holds but the cluster does not list answers 404 once the cluster has answered twice without it and 2 s have passed (a topic just created can take a moment to reach every broker). A failed token check answers 401 with {"status": "error", "reason": ...}, the body the management routes give for it, and a namespace or topic not found answers 404 with 40401. A request without the subject or authorization header answers 422 with FastAPI's {"detail": [...]}.
| Code | Status | Meaning |
|---|---|---|
| 40002 | 400 | The topic name contains a dot |
| 40003 | 400 | The body is not one JSON object in UTF-8 |
| 40004 | 400 | The body holds more than one JSON value |
| 40005 | 400 | A record field is invalid (bad base64, unknown type or field, timestamp without an offset, no key or value) |
| 40301 | 403 | The service is not authorized to write to the topic |
| 40401 | 404 | The namespace or topic is not found, in the namespace or in the cluster |
| 40402 | 404 | partition_id is not 0 |
| 40801 | 408 | The body did not arrive within 20 s |
| 41301 | 413 | The request body is over 16 MiB |
| 41302 | 413 | A record is over 8 MiB, counting Kafka's overhead |
| 41303 | 413 | The body's JSON would take too much memory to parse |
| 41304 | 413 | A batch's records are over 16 MiB once encoded |
| 41501 | 415 | The body is not Content-Type: application/json in UTF-8 |
| 42206 | 422 | The body is empty, or the record needs a schema registry |
| 42208 | 422 | The batch is empty or over 500 entries, or an entry's id is missing, invalid or repeated |
| 42901 | 429 | The service, or your share of it, holds as many produce requests or bytes as it can; retry after Retry-After |
| 50002 | 500 | Kafka refused the record, or the service is shutting down |
| 50003 | 500 | A retriable Kafka error, or the record was not acknowledged within the 30 s; retry it |
| 50302 | 503 | The service cannot reach the Kafka cluster; retry after Retry-After |
In a batch, 40005, 40301, 40401 (from the cluster), 40402, 41302, 42206, 50002, 50003 and 50302 fail the one entry and the others go on; the other codes refuse the whole request. Error messages do not include Kafka's own error text.
Differences from Confluent's REST API¶
The routes follow Confluent's statuses and codes except where noted here.
| Case | Octopus | Confluent | Why |
|---|---|---|---|
| Body over 16 MiB | 413, 41301 | 400, 400 | 413 says what is wrong, and the code tells it apart |
| Record over 8 MiB | 413, 41302 | 413, 413 | A five-digit code like the rest |
| Body not JSON, more than one value, invalid field | 400 with 40003, 40004, 40005 | 400, 400 | One code for each cause |
| Content type not JSON | 415, 41501 | 415, 415 | A five-digit code like the rest |
| More than one JSON value in a single-record body | Refused | Read as a stream of records | One request is one record; several go to records:batch |
Schema fields with type BINARY, JSON or STRING |
Refused, 422 | Ignored | Without a schema registry the fields cannot mean what the client asked |
| Payload errors in a record (a schema type or field, no type) | 422, error_code 42206 |
422, error_code 422 and Error: 42206 : ... in the message |
The code where a client looks for it; Confluent already puts 42206 in error_code for an empty body and 42208 for an invalid batch, as here |
| Token check failed | 401 with status and reason |
401 from the web server's HTTP authentication, without a Confluent error code; Confluent's 40101 is for a failed Kafka authentication | The same body as the management routes |
partition_id above 0 |
404, 40402, at once | Negative ids: 404, 40402. Ids past the partition count: the Kafka producer waits max.block.ms, then fails with "not present in metadata", which Confluent answers with 404, 40401 |
Every topic has one partition, so the answer is known at once |
| No key and no value | Refused, 400 | Sent | kafka-python cannot send it |
| Null header value | Written empty | Written null | kafka-python cannot send a null header value |
| Too many requests or bytes | 429, 42901 | 429, 429 for its own rate limit | Confluent's code for a quota, which this is |
| Topic missing from the cluster | 404, 40401 after 2 s | 404, 40401 after max.block.ms (60 s by default) |
Same reply, sooner |
| Body slower than 20 s | 408, 40801 | No limit of its own; Confluent sends 408, 40801 for a serialization error | A stalled upload would hold its share; the code is Confluent's for a 408 |
| Parse cost, records over 16 MiB | 413 with 41303, 41304 | No such limits | They bound the service's memory |