Kestrel Broker Plumbing: Daily Login, Secret Redaction, Rate Limiters

What happens before a trading system sends its first request? Kestrel has to log in every morning, keep secrets out of every log, and stay under a broker's rate limits across several processes.

Intro

Before the First Request

Before a trading system sends a single order, it has to do three unglamorous things. It has to prove who it is. It has to make sure that proof never leaks into a log. And it has to stay politely under the broker's request limits. Get any of them wrong, and your day ends early.

  • Login
  • Redaction
  • Rate limits

Where This Fits

Last episode, kestrel_md fanned market data out to strategies and fell back to REST when the websocket failed. This episode is the plumbing underneath every broker call. We will cover the daily login, redaction, two cooperating rate limiters, and finally the symbol master and history downloads.

  • kestrel_md recap
  • Shared FyersClient
  • Broker plumbing

The Daily Login

Tokens Die at 06:30 IST

The FYERS access token expires every day at six thirty in the morning, Indian time. There is no refresh token. So each morning the operator runs kestrel_history with the fyers-login flag. It is a deliberate human step, not something hidden in a cron job.

  • Expiry: 06:30 IST daily
  • No refresh token
  • Operator runs login

The Auth URL and State

The login starts by building an authorization URL. It carries a random state value: thirty-two hex characters. Kestrel keeps that value in memory. Whatever comes back from the browser must carry the same state, or it is rejected.

  • Auth URL
  • 32-hex random state
  • Kept in memory

One-Shot Local Redirect

The redirect points at the loopback address, 127.0.0.1. Kestrel starts a one-shot local HttpServer, the same one we met in episode nineteen, and waits up to five minutes for the browser to arrive. It handles one request, then shuts down. No long-lived listener is left behind.

  • Loopback redirect
  • Waits 5 minutes
  • Serves one request

State Must Match

When the redirect arrives, Kestrel compares the returned state with the one it generated. A mismatch means the request did not come from the login we started. This is the classic cross-site request forgery defence. Without it, any web page could push an auth code into your process.

  • Compare state values
  • Mismatch: reject
  • CSRF defence

Exchanging the Auth Code

With a valid code, Kestrel posts to the validate-authcode endpoint. It must send an app id hash. That is the hex encoding of the SHA-256 of the app id, a colon, and the secret. For example, the string is the app id placeholder, colon, secret placeholder, and the output is sixty-four hex characters.

  • sha256(APP_ID:SECRET)
  • Hex encode
  • POST validate-authcode

Verify With Profile

A token in hand is not yet a token that works. Kestrel calls the profile endpoint with it, as a verification step. Only if that succeeds does it persist the token. This way a typo or a half-working login never overwrites yesterday's file with garbage.

  • Call profile
  • Success: write file
  • Failure: keep old file

Writing the Token Safely

Atomic and Private

Writing the token file has two requirements. It must be private, and it must be atomic. Kestrel creates the directory with mode zero seven hundred. It opens a temporary file with O_EXCL and mode zero six hundred, so only the owner can read it, and it refuses to reuse an existing file.

  • Directory mode 0700
  • O_EXCL, mode 0600
  • Owner-only access

fsync, Then Rename

Then it writes, calls fsync so the bytes reach disk, and renames the temp file over the real name. Rename is atomic on a POSIX filesystem. A reader sees either the old complete token or the new complete token, never half of one. A crash in the middle leaves the old file intact.

  • write, fsync, rename
  • Readers never see partial
  • Crash-safe

Redaction

Secrets Leak Through Logs

Most credential leaks are not hacks. They are logs. An error message that includes the request headers. A debug line that prints a response body. A crash report that quotes a URL. Once a token is in a log file, it is in backups, in chat pastes, and in screenshots.

  • Leaks happen in logs
  • Errors, bodies, URLs
  • Logs get copied

Header Redaction

So Kestrel redacts at the boundary. The value of the Authorization header becomes three asterisks. So do the token and the secret as known values. Anything that formats a request for display goes through the redactor first.

  • Authorization becomes ***
  • Known token and secret too
  • Redact before display

Keyword Scrubbing

Known values are not enough, because secrets also appear inside free text. So the redactor scrubs the text that follows keywords such as access token or secret key. This applies to log messages, error strings, and response bodies alike. The cost is a slightly over-eager scrub. The benefit is no leak by omission.

  • Keywords trigger scrubbing
  • Messages, errors, bodies
  • Over-scrub beats leaking

The Client Call

Anatomy of FyersClient::call

Every broker request goes through one function, FyersClient call. It builds the headers, asks the limiters for admission, performs the request through libcurl with a twenty second timeout, and parses the JSON that comes back. One door in means one place to enforce the rules.

  • Headers
  • Limiters admit
  • libcurl, 20 s timeout
  • Parse JSON

The 429 Rule

When the broker answers with HTTP 429, or a body with code minus 429, Kestrel logs that it is not retrying, and records a strike. There are no automatic retries anywhere in the broker layer. A retry loop against a rate limit is how a small problem becomes a day-long block.

  • HTTP 429 or code -429
  • Log: not retrying
  • Record a strike

The Process Limiter

Defaults and Daily Caps

The process limiter has conservative defaults: four requests per second, and ninety per minute. Environment settings can raise these, but they are clamped at eight and one hundred eighty. There are also daily caps: sixty thousand calls overall and nine thousand transactional. These are the code's defaults as of 2026.

  • 4/s and 90/min
  • Env clamp: 8 and 180
  • Daily: 60000 and 9000

Cooldown After a 429

Any 429 starts a cooldown of at least sixty-one seconds, or the Retry-After value if the broker supplies a longer one. Why sixty-one and not sixty? Because the broker's minute window may not line up with ours, and a one-second margin is cheap.

  • Cooldown at least 61 s
  • Or Retry-After
  • 1 s safety margin

The Strike Breaker

Now the harsh rule. After two strikes within one IST day, every call fails immediately until midnight IST. Kestrel calls this the strike breaker. FYERS blocks an account for the day after more than three per-minute violations, so Kestrel stops itself at two, leaving one in reserve.

  • 2 strikes in an IST day
  • All calls fail until 00:00
  • Broker blocks after more than 3

The Sliding Log

Reserving Time Slots

How does the limiter decide when a call may go? It keeps a sliding log of send times, and it hands out slots in the future. Instead of asking whether I can send now, a caller asks for the next legal slot, then sleeps until that moment.

  • Log of send times
  • Reserve a future slot
  • Sleep until the slot

The gateSlot Code

Here is the core of gateSlot. It builds two windows: per second and per minute. For each one, if the log has at least limit entries, the candidate slot is the entry limit places from the end, plus the span. Take the maximum across windows. Also remember how many entries to keep.

  • Two windows
  • slots[size - L] + S
  • Max over windows

Order and Trimming

Two final lines matter. If the limiter has been used and the log is not empty, the slot is never earlier than the previous slot, so reservations stay ordered. Then the slot is pushed, and the oldest entries are popped until only the largest limit's worth remain.

  • Never before previous slot
  • push_back the slot
  • Trim to keep

Worked Example: Fifth Call

Take a limit of four per second. The log holds slots at zero, point one, point two, and point three seconds. A fifth call arrives at point four. The slot four places from the end is zero point zero. Add one second span, and the fifth call gets slot one point zero.

  • Slots: 0.0, 0.1, 0.2, 0.3
  • Call at 0.4 s
  • slots[size - 4] + 1 = 1.0

Worked Example: Sixth Call

Now a sixth call. The fourth most recent slot is point one. Add one second, and the slot is one point one. Notice what happened: the calls spread out naturally. Never more than four in any one-second window, and no call is rejected. It is only delayed.

  • Fourth most recent: 0.1
  • 0.1 + 1 = 1.1
  • Delayed, never dropped

Reserve, Sleep, Re-check

Concurrency adds a subtlety. The slot is reserved while holding the mutex, but the sleeping happens outside it, so other threads can reserve their own slots. After waking, the code re-checks conditions before sending, such as a cooldown or breaker that triggered meanwhile. If the clock jumps by more than two hours, the log is cleared.

  • Reserve under mutex
  • Sleep outside the lock
  • Re-check before sending
  • Clock jump over 2 h clears log

The Account-Wide Limiter

The Broker Counts Per Account

Here is the catch. The broker limits per account, not per process. Kestrel runs kestrel_md, kestrel_orders, kestrel_history and more. If each respects four per second on its own, together they might send far more. A per-process limit alone is the wrong abstraction.

  • Broker limits per account
  • Many Kestrel processes
  • Sum exceeds the limit

A Shared State File

So all processes share one fixed-size binary file under the data root, in run, fyers_rate, named by host. It holds a magic number, a version, a count, a cooldown-until time, a strike day, strikes, and two hundred fifty-six sorted slots. Defaults are eight per second and one hundred fifty per minute.

  • One file per host
  • Header plus 256 slots
  • 8/s and 150/min

Two Processes, One Lock

Every read-modify-write happens under flock with an exclusive lock. Process A takes the lock, reads the slots, reserves a slot, writes back, and releases. Process B waits for the lock only for that short moment. Crucially, the lock is never held across a sleep, so nobody blocks the others while waiting for their own slot.

  • flock(LOCK_EX)
  • Short read-modify-write
  • Never held across a sleep

Death, Staleness and Shared Pain

If a process dies while holding the lock, the kernel releases it, so no stale lock can freeze trading. A sanitize step drops slots that are stale or in the future, which protects against a corrupted file or a wild clock. And a 429 in any process sets the shared cooldown, so all processes pause together.

  • Kernel frees lock on death
  • Sanitize stale and future slots
  • A 429 pauses everyone

Symbol Master and History

The Symbol Master

Now the data side. FYERS publishes public CSV files, the symbol master, for NSE cash, NSE derivatives, BSE cash, BSE derivatives and MCX commodities. Each has twenty-one columns. Kestrel caches them with a twelve hour time to live, so a daily login day does not hammer the download.

  • Five public CSVs
  • 21 columns each
  • 12 hour TTL

Validating the Disk Copy

A cached file on disk is only trusted after validation. Kestrel checks the column count, and a floor on the row count; for NSE derivatives that floor is twenty thousand rows. A truncated or error page saved as a CSV fails the check. If a fresh download fails, a stale but valid copy is used as fallback.

  • Check column count
  • Row floor: NSE_FO 20000
  • Stale fallback

History Windows

For history, the broker caps how much one request may cover. Kestrel's windows are three hundred sixty-six days for daily bars, thirty days for minute bars, and five days for second bars. Longer ranges are split into consecutive windows, fetched one by one through the same limiters.

  • 1D: 366 days
  • Minutes: 30 days
  • Seconds: 5 days

A Year of Minute Bars

The arithmetic is simple. A year of one-minute bars is about three hundred sixty-five days divided by thirty, which is just over twelve, so about thirteen requests. At four per second that is trivial. A sixteen-entry LRU cache avoids refetching windows already seen in this run.

  • 365 / 30 is about 12.2
  • About 13 requests
  • 16-entry LRU

Day States

Each day of history ends up in one of four states. Complete means the full session arrived. Partial means some bars are missing. Empty means a day with no data, such as a holiday. Failed means the request itself failed. Keeping these distinct stops a failure from being stored as an empty day, which would silently poison later backtests.

  • Complete, Partial
  • Empty, Failed
  • Failure is not emptiness

Pitfalls

Three Mistakes to Avoid

Three pitfalls sum up the episode. First, retrying 429s in a loop; Kestrel records a strike and stops. Second, logging raw responses; redact at the boundary. Third, per-process limits when the broker limits per account; share the state through a locked file.

  • No retry loops
  • Never log raw responses
  • Limit per account

Recap

Recap

To recap. The token dies at six thirty daily, so the operator logs in with a state-checked flow and writes the token atomically and privately. Secrets are redacted everywhere. Calls are admitted by a sliding-log limiter, then an account-wide shared one, with a strike breaker. Symbol masters and history are cached and validated.

  • Safe login
  • Redact everywhere
  • Limit across processes

Next: When the Network Lies

Next episode we ask a harder question: what happens when you send an order and the network might lie to you? Uncertain outcomes, tags, and a write-ahead ledger. This is educational content, not financial advice.

  • Episode 23 next
  • Uncertain outcomes
  • Subscribe