Skip to content

FAQ

Frequently asked questions and design decisions


Many RPC protocols (like JSON-RPC 2.0) support “notifications” — requests without an id field where the server processes the request but never sends a response. Mesh intentionally omits this feature.

Notifications provide no feedback. The caller cannot know:

  • Did the function exist?
  • Were the arguments valid?
  • Did processing succeed or fail?
  • Was the request even received?

For internal microservice communication, this lack of feedback creates reliability problems:

// A notification to get a user makes no sense
{
"protocol": { "name": "mesh", "version": "0.1.0" },
"call": {
"function": "users.get",
"version": "1",
"arguments": { "id": 42 }
}
}
// Where does the user data go? Nowhere.

The protocol would allow structurally valid requests that are semantically nonsensical.

For “fire and forget” with tracking:

Use the async extension. The server returns immediately with an operation ID, and the caller can check status later if needed:

// Request
{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_001",
"call": {
"function": "reports.generate",
"version": "1",
"arguments": { "type": "annual" }
},
"extensions": [
{
"urn": "urn:mesh:ext:async",
"options": { "preferred": true }
}
]
}
// Response (immediate)
{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_001",
"result": null,
"extensions": [
{
"urn": "urn:mesh:ext:async",
"data": {
"operation_id": "op_xyz789",
"status": "processing",
"poll": {
"function": "mesh.operation.status",
"version": "1",
"arguments": { "operation_id": "op_xyz789" }
},
"retry_after": { "value": 5, "unit": "second" }
}
}
]
}

For fast acknowledgment:

Functions can validate, queue work, and return immediately:

// Response in <5ms
{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_001",
"result": {
"queued": true,
"job_id": "job_abc123"
}
}

This provides the same speed as notifications, plus confirmation that the request was valid and accepted.

For event emission:

Use message queues directly. If you’re emitting events without needing Mesh’s request/response semantics, the queue transport is more appropriate than wrapping events in Mesh protocol overhead.


How does Mesh handle long-running operations?

Section titled “How does Mesh handle long-running operations?”

Mesh provides first-class support for asynchronous operations. See Async for full details.

Request async processing:

{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_001",
"call": {
"function": "exports.generate",
"version": "1",
"arguments": { "format": "csv" }
},
"extensions": [
{
"urn": "urn:mesh:ext:async",
"options": { "preferred": true }
}
]
}

Server accepts and returns operation ID:

{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_001",
"result": null,
"extensions": [
{
"urn": "urn:mesh:ext:async",
"data": {
"operation_id": "op_xyz789",
"status": "processing",
"poll": {
"function": "mesh.operation.status",
"version": "1",
"arguments": { "operation_id": "op_xyz789" }
}
}
}
]
}

Poll for status:

{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_poll",
"call": {
"function": "mesh.operation.status",
"version": "1",
"arguments": {
"operation_id": "op_xyz789"
}
}
}

Get result when complete:

{
"protocol": { "name": "mesh", "version": "0.1.0" },
"id": "req_poll",
"result": {
"operation_id": "op_xyz789",
"status": "completed",
"result": {
"download_url": "https://..."
}
}
}
  • mesh.operation.status — Check operation status
  • mesh.operation.cancel — Cancel a pending operation
  • mesh.operation.list — List operations for the caller

See System Functions for details.


Why per-function versioning instead of API versioning?

Section titled “Why per-function versioning instead of API versioning?”

Traditional REST APIs version the entire API: /api/v1/, /api/v2/. Mesh versions each function independently.

When you version the entire API, unrelated changes force version bumps everywhere:

/api/v1/orders ← Unchanged, but stuck at "v1"
/api/v1/users ← The actual breaking change
/api/v1/products ← Unchanged, but stuck at "v1"

Consumers must upgrade everything when they only needed one change. Teams must coordinate releases across unrelated domains.

orders.create@1 ← Untouched
users.get@2 ← Evolved independently
products.list@1 ← Untouched

Benefits:

  • Teams evolve functions without coordinating releases
  • Consumers upgrade function-by-function
  • Deprecation is surgical: sunset orders.create@1, not “all of v1”
  • No version sprawl where everything is “v7” but 90% unchanged

See Versioning for full details.


Why is the id field a string, not a number?

Section titled “Why is the id field a string, not a number?”

JSON numbers have precision limits. JavaScript (and many JSON parsers) use IEEE 754 double-precision floats, which can only safely represent integers up to 2^53.

// JavaScript precision loss
JSON.parse('{"id": 9007199254740993}')
// { id: 9007199254740992 } — Wrong!

String IDs avoid this entirely and support richer formats:

  • UUID v4: "550e8400-e29b-41d4-a716-446655440000"
  • Prefixed: "req_abc123xyz"
  • ULID: "01ARZ3NDEKTSV4RRFFQ69G5FAV"

Why does Mesh use error/errors instead of HTTP status codes?

Section titled “Why does Mesh use error/errors instead of HTTP status codes?”

HTTP status codes are transport-level. Mesh is transport-agnostic — it works over HTTP, message queues, or any other transport.

Additionally, HTTP status codes are ambiguous:

  • Is 404 “endpoint not found” or “resource not found”?
  • Is 500 a Mesh error or a proxy error?
  • Is 503 the service or a load balancer?

Mesh uses:

  • HTTP 200 for all Mesh responses (the transport succeeded)
  • error/errors field for Mesh-level errors with explicit codes

Transport-level errors (502, 503, 504) indicate the request never reached Mesh.

See Errors for the full error specification.