Limits & constraints
Every floor, cap, and validation rule SchedStack enforces, and the typed error you get
when you cross one. Each error follows the typed error envelope
({ "error": { "type", "code", "message", "param", "request_id" } }); the code is
shown below.
Timing
Section titled “Timing”| Rule | Limit | Error |
|---|---|---|
Minimum delay |
>= 1s |
422 sub_floor_delay |
delay format |
valid duration (30s, 5m, 24h, 1h30m) |
400 invalid_duration |
fire_at must be in the future |
>= now + 1s |
422 fire_at_in_past |
fire_at horizon |
<= now + 10 years |
422 fire_at_too_far |
fire_at format |
strict RFC 3339 instant | 400 (message) |
local_fire_at format |
wall-clock 2006-01-02T15:04:05 (with timezone) |
400 invalid_duration |
local_fire_at bounds |
same future / 10-year rules | 422 fire_at_in_past / fire_at_too_far |
| Exactly one timing source | one of delay · fire_at · local_fire_at · cron |
422 missing_timing / 400 multiple_timing |
cron |
5-field standard cron (no seconds field) | 422 invalid_cron |
timezone |
a valid IANA name (e.g. America/New_York); default UTC |
422 invalid_cron |
Payloads & headers
Section titled “Payloads & headers”| Rule | Limit | Behavior |
|---|---|---|
| Request body | <= 1 MB |
larger → 413 payload_too_large |
Delivery body |
<= 256 KB |
larger → 413 payload_too_large |
| Total custom headers | <= 16 KB |
larger → 413 payload_too_large |
metadata labels |
<= 50 |
more → 422 too_many_labels |
Two header rules make a delivery terminal (dead-lettered, never retried):
- No control characters. A header name or value containing CR, LF, or other control characters is rejected.
- No hop-by-hop / reserved names.
host,content-length,connection,keep-alive,proxy-*,te,trailer,transfer-encoding, andupgradeare not allowed.
SchedStack sets its own Sched-* and Idempotency-Key headers after yours, so you
cannot override them. See the delivery contract.
Retry policy
Section titled “Retry policy”All bounds are enforced; any violation returns 422 invalid_retry_policy.
| Field | Range | Default |
|---|---|---|
max_attempts |
1–50 | 8 |
base |
0–24h | 5s |
max |
0–168h (7 days) | 1h |
factor |
1–100 | 2 |
strategy |
exponential only |
exponential |
jitter |
boolean | true |
The next attempt is scheduled at base × factor^attempt, capped at max. See
Retries & dead-letter.
TTL & expiry
Section titled “TTL & expiry”ttl is any valid duration with no minimum or maximum. The delivery deadline is
first fire + ttl, and it caps retries: a delivery that can’t land before the deadline
won’t be retried past it. It finalizes as expired (a terminal, replayable state).
Idempotency (API requests)
Section titled “Idempotency (API requests)”The optional request Idempotency-Key header makes a mutation safe to retry.
| Rule | Behavior |
|---|---|
| Window | the first response is stored for 24 hours |
| Scope | (project, mode, key): test and live never collide |
| “Same request” | a fingerprint of method + path + body |
| Same key, different body | 409 idempotency_key_reuse |
| Same key, still in flight | 409 idempotency_in_progress |
| What’s cached | only 2xx/3xx; a 4xx/5xx releases the key so you can retry |
| Replay | returns the stored response with Idempotent-Replayed: true |
| Key length | a free-form string (a UUID is ideal); <= 255 characters. Longer → 400 idempotency_key_too_long |
Applies to: POST /schedules, PATCH /schedules/{id}, /reschedule, POST /endpoints,
PATCH /endpoints/{id}, and /replay. See Idempotency.
Pagination
Section titled “Pagination”| Rule | Behavior |
|---|---|
Page size (limit) |
default 20, max 100 |
Out-of-range limit |
adjusted to a valid limit rather than erroring (an invalid or < 1 value falls back to 20) |
| Cursor | opaque; pass next_cursor back via ?cursor= |
| Invalid cursor | 400 invalid_cursor |
Destinations
Section titled “Destinations”| Rule | Requirement | Error / outcome |
|---|---|---|
| Scheme | HTTPS required | 422 url_blocked |
| Reachability | must be publicly routable: private, loopback, link-local, carrier-grade-NAT, and cloud-metadata addresses (e.g. 169.254.169.254) are rejected |
delivery terminal |
| Redirects | not followed: a 3xx is a terminal misconfiguration, fix the URL |
delivery terminal |
| DNS rebinding | the resolved address is re-checked on every connection | delivery terminal |
| HTTP method | one of POST, PUT, PATCH, GET, DELETE (default POST) |
400 invalid_method |
Endpoint profiles
Section titled “Endpoint profiles”| Rule | Requirement | Error |
|---|---|---|
url |
required, HTTPS | 422 missing_url / url_blocked |
timeout |
> 0 and <= 1h |
422 invalid_timeout |
method with endpoint_id |
omit it: the method comes from the profile | 400 method_on_profile |
endpoint + endpoint_id |
mutually exclusive | 400 multiple_endpoint |
Unknown / archived endpoint_id |
must exist and be active | 422 invalid_endpoint / endpoint_archived |
| Archiving a profile in use | blocked while a schedule references it | 409 profile_in_use |
retry_budget |
rate >= 0, burst >= 0 (default 10 / 100) |
422 invalid_retry_budget |
rate_limit |
per_second >= 0, accepted but not yet enforced |
422 invalid_rate_limit |
breaker_policy |
threshold 1–1000; base_open / max_open / probe_timeout all required, each > 0 and <= 24h |
422 invalid_breaker_policy |
See Endpoint profiles.
Authentication
Section titled “Authentication”| Rule | Behavior | Error |
|---|---|---|
| Header | Authorization: Bearer sk_<mode>_… |
– |
| Missing key | – | 401 missing_api_key |
| Invalid or revoked key | – | 401 invalid_api_key |
| Cross-scope access | a key only sees its own (project, mode) |
404 not_found |
Defaults worth knowing
Section titled “Defaults worth knowing”- Per-attempt delivery timeout: 30 seconds.
- Retry budget: 10 retries/second, burst 100, per
(project, endpoint). - Circuit breaker: opens after 5 consecutive failures; open window 30s → 1h; probes every 30s.
- Retry policy:
max_attemptsdefaults to 8 when you don’t supply a policy.
Error code index
Section titled “Error code index”Every typed code you can receive, with its HTTP status. Branch on type for control flow
and code for specifics (see Conventions).
code |
HTTP | When |
|---|---|---|
invalid_json |
400 | Malformed JSON in the request body |
idempotency_key_too_long |
400 | Idempotency-Key longer than 255 characters |
invalid_duration |
400 | delay / local_fire_at isn’t a valid duration/format |
multiple_timing |
400 | More than one timing source given |
multiple_endpoint |
400 | Both endpoint and endpoint_id given |
method_on_profile |
400 | method sent alongside endpoint_id |
invalid_method |
400 | HTTP method not one of POST/PUT/PATCH/GET/DELETE |
invalid_cursor |
400 | Pagination cursor is not valid |
missing_api_key |
401 | No Authorization bearer key |
invalid_api_key |
401 | Key is unknown or revoked |
not_found |
404 | No such object in this (project, mode) |
idempotency_key_reuse |
409 | Same Idempotency-Key, different request body |
idempotency_in_progress |
409 | A request with the same Idempotency-Key is still in flight |
not_replayable |
409 | The delivery isn’t in a terminal state, so it can’t be replayed |
profile_in_use |
409 | Can’t archive an endpoint profile a schedule still references |
payload_too_large |
413 | Request body >1 MB, delivery body >256 KB, or total headers >16 KB |
sub_floor_delay |
422 | delay below the 1 s floor |
fire_at_in_past |
422 | fire_at / local_fire_at is not in the future |
fire_at_too_far |
422 | fire_at / local_fire_at is more than 10 years out |
missing_timing |
422 | No timing source given |
invalid_cron |
422 | cron expression or timezone is invalid |
too_many_labels |
422 | More than 50 metadata labels |
url_blocked |
422 | Destination isn’t HTTPS or isn’t publicly routable |
invalid_retry_policy |
422 | A retry_policy field is out of range |
missing_url |
422 | Endpoint profile has no url |
invalid_endpoint |
422 | endpoint_id doesn’t exist |
endpoint_archived |
422 | endpoint_id refers to an archived profile |
invalid_timeout |
422 | Profile timeout is <= 0 or > 1h |
invalid_retry_budget |
422 | Profile retry_budget values are invalid |
invalid_rate_limit |
422 | Profile rate_limit value is invalid |
invalid_breaker_policy |
422 | Profile breaker_policy is incomplete or out of range |