Falcon errors & retries
Every Falcon rejection by transport: JSON-RPC codes and messages, raw HTTP statuses, QUIC SubmitError codes, UDP silence, and what to retry.
The same rejection looks completely different depending on how you submitted it.
| Transport | Where the error appears |
|---|---|
| JSON-RPC | error object in the body, usually with HTTP 200 |
/binary, /plaintext | HTTP status + a short plain-text body |
| QUIC | SubmitError from falcon-client, or a connection close with a reason string |
| Native UDP | Nowhere. Nothing is sent back for any reason |
JSON-RPC errors
Falcon answers with HTTP 200 for almost every JSON-RPC error. Only two carry a matching status.
| Code | HTTP | Message | Cause | Fix |
|---|---|---|---|---|
-32700 | 200 | parse error: <detail> | Body is not valid JSON | Send a valid JSON object |
-32600 | 200 | invalid request | Not a POST, body is not a JSON object, or jsonrpc is missing | POST a JSON-RPC 2.0 object |
-32600 | 200 | unsupported jsonrpc version | jsonrpc is not "2.0" | Set "jsonrpc":"2.0" |
-32600 | 200 | missing method | No method field | Add sendTransaction or getVersion |
-32600 | 200 | request body too large (limit 3072 bytes) | Body over 3 KiB | Shrink the envelope; the transaction itself is capped at 1,232 bytes |
-32600 | 200 | request body read timed out | The body never finished arriving | Resend the same signed transaction |
-32601 | 200 | method not found | Any method other than the two supported | Falcon is not a full RPC; use your Solana RPC |
-32602 | 200 | See the parameter table below | The request or the transaction is invalid | Fix and re-sign; do not retry identical bytes |
-32603 | 200 | server configuration error: tip accounts not set | Server-side tip configuration | Report on Discord |
-32603 | 200 | preflight check failed: RPC node returned an error | Upstream simulation RPC errored | Retry, or submit with skipPreflight: true |
-32603 | 200 | preflight check timed out | Simulation exceeded its timeout | Retry, or submit with skipPreflight: true |
-32000 | 401 | unauthorized | api-key missing, not a UUID, or not a live key | Fix the key; do not retry |
-32001 | 429 | rate limit exceeded | Over your per-second submission allowance | Slow down, then resend the same bytes |
-32002 | 200 | Transaction simulation failed: <error> | Preflight simulation returned an error | Read data, fix the transaction |
-32602 messages
| Message | Meaning |
|---|---|
expected params array in request body | params missing or not an array |
first parameter must be a base58 or base64 encoded transaction string | params[0] is not a string |
unsupported encoding | encoding is neither base58 nor base64 |
transaction string is not valid base58 | Usually base64 sent without "encoding":"base64" |
transaction string is not valid base64 | Malformed base64 |
transaction exceeds maximum size of 1232 bytes | Decoded transaction over the cap |
transaction has no valid signature | Unsigned, or a signature count of 0 |
could not deserialize transaction: verify encoding and format | Not a parseable Solana transaction, or extra trailing bytes |
transaction signature count does not match required signatures | With "data":{"expected":n,"actual":n} |
transaction does not include required tip | With "data":{"minimumLamports":n}; see Tips |
-32002 carries the whole simulation result in data, including err, logs and unitsConsumed.
Raw HTTP errors
/binary and /plaintext answer with a status and a short plain-text body. Several causes share a status, so the body is the only thing that separates them.
| Status | Body | Cause | Retry? |
|---|---|---|---|
200 | base58 signature | Accepted and forwarded | — |
400 | failed to read body | Connection broke mid-body | Yes |
400 | invalid utf8 | /plaintext body is not UTF-8 text | No |
400 | invalid base64 | /plaintext body is not valid base64 | No |
400 | transaction has no valid signature | Unsigned transaction | No |
400 | could not deserialize transaction | Not a parseable transaction | No |
400 | transaction signature count does not match required signatures | Missing a signer | No |
400 | transaction does not include required tip | Tip missing or below the minimum | No |
401 | unauthorized | API key missing, malformed or revoked | No |
405 | method not allowed | Used a verb other than POST | No |
408 | body read timed out | Body did not arrive in time | Yes |
413 | request body too large | Over 1,232 bytes on /binary, or over 2,048 on /plaintext | No |
413 | decoded transaction too large | /plaintext base64 decodes to more than 1,232 bytes | No |
429 | rate limited | Over your per-second allowance | Yes, after backing off |
No HTTP route reports a forwarding failure. Once validation passes you get 200 and a signature, whatever the forward does.
QUIC rejections
Server rejections reach you as a SubmitError. On the wire that's two bytes: 0x01, then the code.
| Code | SubmitError | Meaning | Retry? |
|---|---|---|---|
0x00 | — | Accepted | — |
0x01 | RateLimited | Over your per-second allowance | Yes, after backing off |
0x02 | Unsigned | Transaction has no valid signature | No |
0x03 | MissingTip | Tip missing or below the minimum | No |
0x04 | DeserializeFailed | Bytes are not a parseable transaction | No |
0x05 | TooLarge | Over 1,232 bytes | No |
0x06 | ForwardFailed | Defined in falcon-client, but the server does not currently return it; once validation passes the stream ack is always Accepted, whatever the forward does | Yes, same bytes |
0x07 | SignatureCountMismatch | Signature count ≠ required signers | No |
0x08 | Unauthorized | Key revoked or no longer authorized | No |
| other | Unknown(code) | Newer server rejection | Log the code and ask on Discord |
use falcon_client::SubmitError;
match client.send_transaction(&transaction).await {
Ok(()) => {}
Err(error) => match error.downcast_ref::<SubmitError>() {
Some(SubmitError::RateLimited) => {
// Back off, then resubmit the same signed bytes.
}
Some(rejection) => {
// Permanent: fix the transaction.
eprintln!("falcon rejected the transaction: {rejection}");
}
None => {
// Transport-level: timeout, MTU, connection lost.
eprintln!("falcon transport error: {error}");
}
},
}An Err with no SubmitError inside is a transport problem rather than a rejection, and by the time you see it the SDK has already reconnected and retried once.
Connection-level failures
When the server closes the QUIC connection, it gives you a readable reason:
| Reason string | Cause | Fix |
|---|---|---|
Missing API key in client certificate CN. Support: discord.gg/corvus-labs | The client certificate carried no UUID | Use falcon-client, or set the certificate CN to your key |
Invalid or revoked API key. Support: discord.gg/corvus-labs | Key is not live | Check the key on Discord |
Connection limit reached. Close unused connections or upgrade. Support: discord.gg/corvus-labs | Too many concurrent connections for this key | Reuse one long-lived client instead of connecting per submission |
Individual streams also get reset when one connection has too many in flight at once; cap your concurrent sends rather than opening more connections.
Native UDP
Native UDP has no error surface at all. Bad frame, bad key, oversized transaction, missing tip, forward failure: every one is the same silence. The full drop list is on Native UDP.
Retry decision table
| Condition | Retry? | How |
|---|---|---|
401 / -32000 / SubmitError::Unauthorized | No | Fix the key |
| Encoding, size, signature or tip rejection | No | Fix the transaction and re-sign; identical bytes fail identically |
-32002 preflight simulation failed | No | Read the simulation logs and fix the transaction |
429 / -32001 / SubmitError::RateLimited | Yes | Slow to your allowance, then resend the same bytes |
408, -32600 request body read timed out, -32603 preflight errors | Yes | Resend the same signed bytes |
SubmitError::ForwardFailed, connection lost, socket error | Yes | Resend the same signed bytes |
| Accepted but not landing | Yes | Resend the same bytes while the blockhash is valid |
| Blockhash expired | No | Rebuild and re-sign; this is a new signature |
Stop on blockhash expiry rather than on an attempt count.
Duplicates
Falcon keeps a signature-keyed cache of recent submissions and short-circuits a duplicate instead of forwarding it again. That only takes effect after a submission of that signature has been delivered, so a copy arriving while the first is still in flight is forwarded too. That's why resending the same signed bytes to chase a landing is expected behaviour. Delivery still isn't exactly-once in either direction.
Acceptance is not landing
Falcon's answer never depends on the outcome of the forward, on any transport.
| Signal | What it proves |
|---|---|
JSON-RPC result / HTTP 200 + signature | Validation passed and the forward was dispatched |
QUIC stream Ok(()) | The server accepted it, or the datagram was already queued and the ack was lost |
QUIC datagram-mode Ok(()) | A packet was queued on your machine |
UDP send() returning without error | Your kernel accepted the datagram |
getSignatureStatuses showing the signature | It landed |
Record both sides. Telemetry has the fields to keep.
Falcon native UDP
The exact Falcon UDP datagram frame for Solana transactions, every silent drop reason, and how to retry when nothing ever answers.
Falcon regions & endpoints
The nine Falcon metros for Solana transaction submission, with the exact host and port for JSON-RPC, HTTP, QUIC and native UDP.