Large payload offload
Large payload offload keeps oversized serialized request, event, response, and stream-item bodies outside a messaging transport. It is disabled by default. Inline envelopes are unchanged when the feature is disabled or when a body is at or below the configured threshold.
Configure a Redis store
For local Redis conformance testing, start the repository's Redis service and run the provider test with its connection string:
docker compose -f samples/docker-compose.yml up -d --wait redis
PowerShell:
$env:ANYPROTOCOL_REDIS_CONNECTION = "localhost:6379"
dotnet test tests/AnyProtocol.Storage.Redis.Tests/AnyProtocol.Storage.Redis.Tests.csproj
POSIX shells:
ANYPROTOCOL_REDIS_CONNECTION=localhost:6379 \
dotnet test tests/AnyProtocol.Storage.Redis.Tests/AnyProtocol.Storage.Redis.Tests.csproj
Register the provider before AddAnyProtocol, then enable an explicit policy:
builder.Services.AddAnyProtocolRedisPayloadStore(
"large-payloads",
options =>
{
options.ConnectionString = redisConnectionString;
options.KeyPrefix = "orders:production:payload";
});
builder.Services.AddAnyProtocol(link => link
.UseSerializer(new TextJsonMessageSerializer())
.UseLargePayloadOffload(options =>
{
options.StoreName = "large-payloads";
options.MaxInlinePayloadBytes = 512 * 1024;
options.MaxStoredPayloadBytes = 64 * 1024 * 1024;
options.TimeToLive = TimeSpan.FromMinutes(15);
})
.AddKafka(kafkaOptions));
All four policy values are required. MaxStoredPayloadBytes also bounds the cumulative serialized
bytes produced or consumed by one stream. Select limits from broker bounds, Redis capacity,
application concurrency, and the maximum retry/recovery window; the example values are not
framework defaults.
Enabling a policy with no matching ILargePayloadStore, duplicate logical store names,
non-positive limits/TTL, or a maximum that is not greater than the inline threshold fails startup.
The store name is logical and case-insensitive.
Wire and delivery behavior
AnyProtocol serializes first. Bodies above the inline threshold are stored under a deterministic
message-ID-derived key before transport publication. The broker or native transport receives an
empty body plus cl-payload-mode=stored, store name, opaque key, length, SHA-256, and expiry
headers. Receivers load and verify the body before deserialization, filters, or handler dispatch.
The same message ID and digest is an idempotent write. Reusing an ID for different content fails
with payload_id_conflict. A transport failure after a successful write leaves the object for TTL
expiry; immediate deletion could race an uncertain acknowledgement or retry. Reads never delete,
so duplicate delivery, competing consumers, retries, and event fan-out remain safe.
Stable failure codes are:
| Code | Meaning | Retryable |
|---|---|---|
payload_not_found |
Missing or expired reference | No |
payload_store_unavailable |
Temporary provider failure | Yes |
payload_integrity_failed |
Length, digest, or provider record mismatch | No |
payload_id_conflict |
Same message ID, different content | No |
payload_store_not_configured |
Receiver lacks the named provider | No |
payload_reference_invalid |
Required reference metadata is malformed | No |
payload_too_large |
Per-message or total-stream bound exceeded | No |
Redis sizing and security
- Use a key prefix that separates application, environment, and tenant where applicable.
- Keep TTL at least as long as broker retries, dead-letter recovery, and expected consumer outages.
- Budget Redis memory for payload size multiplied by peak retained concurrency; Redis is best for moderate, short-lived bodies. Add an object-storage provider for larger or longer-lived objects.
- Restrict Redis with network isolation, TLS where available, ACLs, and least-privilege credentials.
- Never put connection strings or endpoints into headers. The Redis provider hashes message IDs in physical keys and does not log bodies, credentials, or full locators.
- Authorization is evaluated after hydration through the normal server pipeline. Length and SHA-256 are verified before deserialization.
The current serializer and store contracts use ReadOnlyMemory<byte>, so a complete serialized
body is resident in memory at sender and receiver. Offload reduces broker load; it is not unbounded
streaming storage.