[{"data":1,"prerenderedAt":3423},["ShallowReactive",2],{"/projects/celeris-realtime":3,"/projects/celeris-realtime--related":658},{"id":4,"title":5,"body":6,"category":642,"date":643,"description":644,"extension":645,"image_url":64,"link":40,"meta":646,"navigation":647,"path":648,"seo":649,"stem":650,"tags":651,"__hash__":657},"projects/projects/celeris-realtime.md","Celeris Realtime",{"type":7,"value":8,"toc":627},"minimark",[9,15,19,23,27,57,65,70,73,77,80,83,86,89,92,95,98,101,104,107,110,113,116,119,126,129,132,135,139,142,145,148,151,154,157,160,163,167,170,173,181,184,276,279,285,288,292,295,298,301,304,307,310,313,316,320,323,326,329,332,335,342,345,348,351,354,358,361,364,367,372,375,378,381,384,387,390,393,396,400,403,406,409,412,419,422,425,428,432,435,438,441,444,447,450,453,456,460,463,532,535,538,541,544,547,550,553,557,560,563,570,573,580,583,590,593,596,599,602,605,609,612,615,618,621,624],[10,11],"content-time-detail",{"className":12,"date":14},[13],"mb-2","2026-07-29",[16,17,5],"h1",{"id":18},"celeris-realtime",[20,21,22],"p",{},"Celeris Realtime is a public-beta, binary-first WebSocket platform for moving messages across\nchannels, nodes, and regions. I built it in Rust as the sole architect, engineer, and operator,\ncovering the protocol, SDKs, control plane, infrastructure, and observability.",[24,25],"tag-list",{":tags":26},"[\"Rust\", \"Tokio\", \"Actix Web\", \"WebSockets\", \"Kafka\", \"Redis\", \"ClickHouse\", \"PostgreSQL\", \"Kubernetes\", \"Terraform\", \"AWS\", \"Nuxt 4\", \"Vue 3\"]",[28,29,37],"div",{"className":30},[31,32,33,34,35,36],"flex","flex-wrap","items-center","gap-3","mt-4","mb-8",[38,39,50,53],"a",{"href":40,"rel":41,"className":43,"target":49},"https://useceleris.com/",[42],"nofollow",[44,45,46,47,48],"button","button-xs","xs:button-sm","sm:button-base","button-amberwood","\\_blank",[20,51,52],{},"Visit Celeris",[54,55],"content-icon",{"icon":56},"mdi:external-link",[58,59],"content-main-image",{"alt":60,"className":61,"image":64},"Celeris public beta homepage showing its realtime transport positioning",[62,63],"my-12","h-[38vh]","/projects/celeris-realtime/main.webp",[66,67,69],"h2",{"id":68},"where-the-idea-came-from","Where the idea came from",[20,71,72],{},"Celeris did not start as a plan to build another realtime platform. It started because I kept\nrunning into the same problem from different directions.",[28,74],{"className":75},[76],"mb-3",[20,78,79],{},"While I was building the Payaza Web SDK, the backend engineers were occupied with more pressing\nlaunch work, so I built a separate TypeScript, Node.js, and Socket.IO proxy cluster to give the SDK\nthe API and realtime communication it needed. The SDK and proxy worked together: backend services\ncould send payment-status updates through webhooks, and the proxy cluster broadcast those updates\nto connected clients. I also used consistent hashing so requests for the same connection group\nwould keep returning to the right part of the cluster.",[28,81],{"className":82},[76],[20,84,85],{},"That solved the immediate launch problem, and it worked consistently for the traffic pattern we\nhad. Each client was waiting for a response from one backend, though, so it did not expose the\nhardest version of the scaling problem. The question of how I would build a cheap, reliable\nrealtime system that I could reuse elsewhere stayed with me.",[28,87],{"className":88},[76],[20,90,91],{},"When I got the opportunity to join HeySummit, part of the attraction was the chance to work on a\nplatform where many people could be communicating in realtime. It was the larger version of the\nproblem I had been thinking about since Payaza.",[28,93],{"className":94},[76],[20,96,97],{},"At HeySummit, one Firebase Realtime Database was handling realtime communication as the company\ngrew to thousands of concurrent users. The server started becoming unstable, and we were reaching\nthe upper limits of the service. My task was simple to describe, even if the implementation was\nnot: make the delivery path scale.",[28,99],{"className":100},[76],[20,102,103],{},"The solution was to use Firestore as the primary datastore and treat Realtime Database instances\nas realtime delivery servers. We moved selected data to an instance according to its current load\nand the number of participants on the platform. That gave us a practical way to distribute the\nwork instead of asking one database to carry every connected user.",[28,105],{"className":106},[76],[20,108,109],{},"The system worked, but it did not give us a clean way for the same channel to span multiple\nrealtime servers. Data also existed in two places, which meant routing logic, replication logic,\nand more failure paths. We solved the scaling problem in front of us, but I still wanted to\nimplement the underlying transport myself and answer what would happen when we needed to scale\npast that design.",[28,111],{"className":112},[76],[20,114,115],{},"Those two experiences left me with a more specific question: what would a cheap, reliable realtime\nlayer look like if the application database did not have to sit in the middle of every delivery?\nI wanted an answer I could take with me instead of solving the same problem again at every company.",[28,117],{"className":118},[76],[20,120,121,122,125],{},"I was also learning Rust at the time, so I started building the answer with Actix Web, Tokio,\nKafka, and Redis. The first version was a serious learning project, but it was also shaped by the\nbusiness questions I had started paying more attention to: infrastructure cost, reliability, and\nthe experience customers have when systems slow down or fail. In 2026, I began to see it as\nsomething I could make public. That is how the project became\n",[38,123,5],{"href":40,"rel":124},[42],", which is now in public beta. It is also why I have\nstarted extracting reusable parts of the work into open-source libraries.",[28,127],{"className":128},[76],[20,130,131],{},"I am the sole architect, engineer, and operator. That means I have worked on the protocol, data\nmodel, realtime server, control-plane API, dashboard, SDKs, infrastructure, deployments,\nobservability, tests, documentation, benchmarks, and the smaller open-source libraries that came\nout of the work.",[28,133],{"className":134},[36],[66,136,138],{"id":137},"what-celeris-is-and-what-it-deliberately-is-not","What Celeris is, and what it deliberately is not",[20,140,141],{},"Celeris is a binary-first WebSocket transport. An application publishes a message, and Celeris\nmoves it to the connected clients that should receive it. It handles connections, authentication,\nchannels, fan-out, presence, ordering boundaries, replay from a short buffer, and delivery across\nnodes and regions.",[28,143],{"className":144},[76],[20,146,147],{},"It is not the customer's application database, and it is not intended to become one. Celeris does\nnot need to understand an order, payment, chat message, dashboard event, or multiplayer state. It\nonly needs to authenticate the connection, enforce its scope, and move the bytes.",[28,149],{"className":150},[76],[20,152,153],{},"The wire path is binary safe. Clients can publish raw bytes, MessagePack, or Protobuf without\nconverting everything to JSON first. The SDKs provide convenient helpers, but the service remains\na normal WebSocket service underneath them. A team can use the JavaScript, Java, Rust, Python, or\nFlutter SDK, or connect with a raw WebSocket client if that fits better.",[28,155],{"className":156},[36],[20,158,159],{},"That boundary is important to me. An SDK should make the common path easier, not turn a standard\nprotocol into something that only one library can speak.",[28,161],{"className":162},[36],[66,164,166],{"id":165},"the-payload-is-none-of-my-business","The payload is none of my business",[20,168,169],{},"One thing I could not stop thinking about was how much time realtime systems spend serializing and\ndeserializing data they do not need to understand. If Celeris is moving a payment update, a game\nevent, or a document change, the transport should not need to turn that payload into its own object\nbefore sending it to the next connection.",[28,171],{"className":172},[76],[20,174,175,176,180],{},"I built a length-prefixed protocol inspired by Redis RESP. The parser reads the command envelope,\nuses the declared payload length to find its boundary, and keeps the payload as a ",[177,178,179],"code",{},"bytes::Bytes","\nslice. It does not inspect or copy the application data just to discover where the message ends.\nThat is why the parsing time stays close to 0.3 μs as the payload grows in the recorded\nCriterion benchmark.",[28,182],{"className":183},[76],[185,186,187,207],"table",{},[188,189,190],"thead",{},[191,192,193,198,204],"tr",{},[194,195,197],"th",{"align":196},"right","Payload",[194,199,200,203],{"align":196},[177,201,202],{},"serde_json"," deserialization",[194,205,206],{"align":196},"Celeris protocol parser",[208,209,210,222,233,244,255,266],"tbody",{},[191,211,212,216,219],{},[213,214,215],"td",{"align":196},"128 B",[213,217,218],{"align":196},"303 ns",[213,220,221],{"align":196},"300 ns",[191,223,224,227,230],{},[213,225,226],{"align":196},"256 B",[213,228,229],{"align":196},"313 ns",[213,231,232],{"align":196},"305 ns",[191,234,235,238,241],{},[213,236,237],{"align":196},"1 KiB",[213,239,240],{"align":196},"2.7 μs",[213,242,243],{"align":196},"302 ns",[191,245,246,249,252],{},[213,247,248],{"align":196},"64 KiB",[213,250,251],{"align":196},"11 μs",[213,253,254],{"align":196},"304 ns",[191,256,257,260,263],{},[213,258,259],{"align":196},"128 KiB",[213,261,262],{"align":196},"21 μs",[213,264,265],{"align":196},"309 ns",[191,267,268,271,274],{},[213,269,270],{"align":196},"256 KiB",[213,272,273],{"align":196},"38 μs",[213,275,232],{"align":196},[28,277],{"className":278},[76],[20,280,281,282,284],{},"At 128 KiB, that is about 21 microseconds for ",[177,283,202],{}," and 309 nanoseconds for the Celeris\nparser, roughly 68 times faster in this test. It is not a claim that the two operations have the\nsame semantics. JSON deserialization validates and materializes the payload, while Celeris\ndeliberately refuses to understand it. That difference is the point.",[28,286],{"className":287},[36],[66,289,291],{"id":290},"accounts-apps-channels-and-segments","Accounts, apps, channels, and segments",[20,293,294],{},"The control model starts with an account. An account represents an organisation, and it contains\nisolated applications. Each application has its own signing credentials, limits, configuration,\nand usage records.",[28,296],{"className":297},[76],[20,299,300],{},"Connections join channels. A channel is the top-level realtime namespace inside an application,\nwhile segments provide smaller rooms within that channel. A collaboration product could use one\nchannel for a document and segments for different types of activity. A commerce product could use\none channel for an order and separate segments for payment, fulfilment, and internal operational\nupdates.",[28,302],{"className":303},[76],[20,305,306],{},"The customer's backend signs a short-lived connection payload with the application's signing key.\nThe payload can restrict which channel and segments a client may read from or write to. Celeris\ncan validate that signature without calling the customer's database during the WebSocket\nhandshake.",[28,308],{"className":309},[36],[20,311,312],{},"This does not remove application-level authorization. The customer still decides whether a user\nshould receive a token. It removes that database lookup from Celeris's connection hot path after\nthe decision has already been made.",[28,314],{"className":315},[36],[66,317,319],{"id":318},"one-message-three-delivery-paths","One message, three delivery paths",[20,321,322],{},"The simplest delivery path is also the most common one to keep cheap. If the publisher and\nsubscribers are connected to the same Celeris node, the message stays in memory. The channel\nserver writes it into the relevant segment buffer and wakes the local connections that need to\nread it. Kafka is not involved, and a channel topic is not created just because one node has local\nsubscribers.",[28,324],{"className":325},[76],[20,327,328],{},"The second path appears when the same channel is active on more than one node in a region. Redis\nstores the presence and coordination data that lets nodes know where a channel is active. Once a\nsecond node needs the channel, Celeris creates the regional Kafka topic, subscribes the active\nnodes, and uses that topic to carry messages between them.",[28,330],{"className":331},[76],[20,333,334],{},"The third path is the same idea across regions. A Celeris node has access to the configured Kafka\nclusters, but it only publishes a channel message to regions with active subscribers. A region\nthat is configured but has nobody listening to that channel does not receive a copy. This keeps\nregional isolation as the normal case while still allowing a channel to span the United States\nand European Union when an application needs it.",[336,337],"content-image",{"alt":338,"className":339,"image":341},"Paper-craft diagram showing local delivery, Redis coordination, and a Kafka bridge to an active remote region",[340],"my-10","/projects/celeris-realtime/active-region-delivery.webp",[20,343,344],{},"HAProxy sits in front of the realtime nodes and uses the channel reference for consistent routing.\nKeeping the same channel together makes the local path more likely. The nodes also expose load\ninformation used to adjust routing weight from CPU and memory pressure. This is not a promise that\na channel can never span nodes. It is a way to avoid paying the distributed path when the local\none is enough.",[28,346],{"className":347},[36],[20,349,350],{},"Kafka becomes the message backplane when distribution is actually required, not the first stop\nfor every message.",[28,352],{"className":353},[36],[66,355,357],{"id":356},"slow-clients-should-not-become-the-clock","Slow clients should not become the clock",[20,359,360],{},"A shared queue is easy until one connection becomes slow. If the queue only advances when every\nreader has finished, one faulty phone connection can hold back an otherwise healthy channel.",[28,362],{"className":363},[76],[20,365,366],{},"Celeris handles this by giving each client a pointer into its channel segment's shared buffer.\nThe buffer can keep accepting and serving messages while each connection advances from its own\nposition. Faster clients do not have to wait for the slowest one. Faulty connections are cleaned\nup separately rather than being allowed to decide the channel's pace.",[336,368],{"alt":369,"className":370,"image":371},"Paper-craft diagram showing three clients with independent pointers into one shared channel-segment buffer",[340],"/projects/celeris-realtime/independent-readers.webp",[20,373,374],{},"The public product contract keeps up to 100 recent messages for two minutes, including the\ndelivery state needed to serve them. A client can connect without replay, request the available\nbacklog, or request messages within a smaller time window. Replay is opt-in. A client that did not\nask for old messages should not receive them simply because another node is catching up.",[28,376],{"className":377},[76],[20,379,380],{},"There are some ordering limits worth stating plainly. Messages remain ordered within a segment on\none node, and messages from one origin node keep their order when they move through Kafka to\nanother node or region. If two origin nodes publish concurrently, the relative order between\nthose two streams is best effort. Celeris does not claim a global order that the architecture does\nnot actually establish.",[28,382],{"className":383},[76],[20,385,386],{},"The transition from one active node to several is also at least once. A message published in the\nsmall window where another node joins is not meant to disappear, but the normal live path and the\nbacklog relay can both carry it. That means a duplicate is possible during the transition.\nMessage identifiers let consumers handle that case when deduplication matters.",[28,388],{"className":389},[36],[20,391,392],{},"Exactly once would sound neater in a feature list. It would also hide the trade-off I actually\nmade, which was to prefer a possible duplicate over silently losing a message while the topology\nwas changing.",[28,394],{"className":395},[36],[66,397,399],{"id":398},"separate-data-for-separate-jobs","Separate data for separate jobs",[20,401,402],{},"PostgreSQL stores the transactional control-plane data: accounts, plans, applications, signing\nconfiguration, limits, and the other records that define how the service should behave. Those\nrecords need relational constraints, migrations, and transactional updates.",[28,404],{"className":405},[76],[20,407,408],{},"ClickHouse stores usage history. Message counts, connection minutes, channel minutes, and other\ntime-based measurements have a different access pattern from account configuration. Keeping them\nseparate means the account database does not also have to be the analytics database.",[28,410],{"className":411},[76],[20,413,414,415,418],{},"The short realtime buffer is not durable message history. If a customer needs every accepted\nmessage in long-term storage, Bring Your Own Destination connectors can write it to the\ncustomer's Kafka, RabbitMQ, S3, or HTTP endpoint. The export side is at least once and includes an\n",[177,416,417],{},"event_id"," so the destination can deduplicate retries.",[28,420],{"className":421},[36],[20,423,424],{},"This also keeps Celeris from becoming the owner of data it does not need. The platform handles the\nspike and delivers the event. The customer's own systems can process and retain it at their pace.",[28,426],{"className":427},[36],[66,429,431],{"id":430},"operating-the-whole-platform","Operating the whole platform",[20,433,434],{},"The realtime service is written in Rust with Tokio and Actix Web. The customer-facing dashboard\nuses Nuxt 4 and Vue 3. I package the services with Docker, publish images to GitHub Container\nRegistry through GitHub Actions, and provision the AWS infrastructure with Terraform.",[28,436],{"className":437},[76],[20,439,440],{},"The regional environment uses Amazon EKS, EC2, ElastiCache for Redis, and Amazon MSK for Kafka.\nRoute 53 and AWS Global Accelerator handle the global entry path. Kubernetes handles service\nplacement and recovery inside a region, while the application still interacts with Redis, Kafka,\nand PostgreSQL through their normal protocols. I try to use managed services without making the\napplication depend on an AWS-only interface when the standard one is sufficient.",[28,442],{"className":443},[76],[20,445,446],{},"OpenTelemetry instruments the request and message paths. Telemetry flows through Grafana Alloy\ninto Prometheus, Loki, Tempo, and Grafana, giving me metrics, logs, and traces from the same\nsystem. For a distributed message path, a single aggregate throughput figure is not enough. I\nneed to see where time was spent, whether one node is doing unusual work, and what happened around\na connection or publish failure.",[28,448],{"className":449},[36],[20,451,452],{},"Owning all of this alone makes consistency more important. Infrastructure, application code,\ndashboards, SDKs, and documentation cannot each describe a different platform and still be\noperable by one person.",[28,454],{"className":455},[36],[66,457,459],{"id":458},"the-benchmark-i-can-defend","The benchmark I can defend",[20,461,462],{},"The most complete historical load test I still have ran for 25 minutes with a 0.5 KiB payload on\none Celeris node.",[185,464,465,475],{},[188,466,467],{},[191,468,469,472],{},[194,470,471],{},"Measurement",[194,473,474],{},"Recorded result",[208,476,477,485,493,500,508,516,524],{},[191,478,479,482],{},[213,480,481],{},"Throughput",[213,483,484],{},"96,114 messages per second",[191,486,487,490],{},[213,488,489],{},"Duration",[213,491,492],{},"25 minutes",[191,494,495,497],{},[213,496,197],{},[213,498,499],{},"0.5 KiB",[191,501,502,505],{},[213,503,504],{},"Celeris node",[213,506,507],{},"c8g.large (2 vCPUs, 4 GiB memory)",[191,509,510,513],{},[213,511,512],{},"Resource use",[213,514,515],{},"102 MiB memory and 56% CPU",[191,517,518,521],{},[213,519,520],{},"Round-trip latency",[213,522,523],{},"32 ms p95, 13.85 ms average, 12 ms median",[191,525,526,529],{},[213,527,528],{},"Data transfer",[213,530,531],{},"Approximately 44,000 KB/s",[28,533],{"className":534},[76],[20,536,537],{},"The latency includes the k6 client and load-generator overhead. The more important limitation is\nthat the machine generating the load failed before the Celeris node became stressed. So the\n96,114 messages per second result is a sustained tested operating point, not a capacity ceiling.",[28,539],{"className":540},[76],[20,542,543],{},"I expect the real ceiling to be higher because the node still reported 56% CPU and 102 MiB of\nmemory at that point, but expectation is not a measurement. I also lost the rest of the original\ntest documentation, including some of the topology and workload details I would want before\nmaking a broader performance claim.",[28,545],{"className":546},[36],[20,548,549],{},"The next benchmark needs a stronger load generator, recorded connection counts, a saved k6\nconfiguration, node and HAProxy topology, regional placement, and archived raw results. Until I\nrun it, the older number stays exactly what it is: the best operating point I can currently\ndefend.",[28,551],{"className":552},[36],[66,554,556],{"id":555},"the-smaller-tools-that-escaped","The smaller tools that escaped",[20,558,559],{},"Building the whole platform kept exposing infrastructure code I did not want to write again.\nSome of those pieces became independent open-source projects.",[28,561],{"className":562},[76],[20,564,565,569],{},[38,566,568],{"href":567},"/projects/trypema-rate-limiter","Trypema"," began with Celeris pricing tiers. I needed high-throughput\nrate limiting, but I also wanted the system to suppress traffic progressively as it approached a\nlimit instead of changing from fully open to fully closed in one step. It now provides local,\nRedis, and hybrid providers as an independently maintained Rust crate.",[28,571],{"className":572},[76],[20,574,575,579],{},[38,576,578],{"href":577},"/projects/distkit","distkit"," came from the distributed counters and locks I kept needing around the\nplatform. It includes strict counters for immediate consistency, buffered counters for workloads\nthat can trade some freshness for throughput, instance-aware counters, and Redis-backed\ndistributed mutex and read-write locks.",[28,581],{"className":582},[76],[20,584,585,589],{},[38,586,588],{"href":587},"/projects/laye","Laye"," came from repeating authorization rules across the control-plane and\nsupporting services. It keeps role and permission policies separate from token decoding and\ndatabase access, then adapts those policies to Actix Web and Tower-based frameworks.",[28,591],{"className":592},[76],[20,594,595],{},"All three are maintained independently. They have their own releases, documentation, tests, and\nrepositories. They are also used in Celeris, which keeps them tied to the\nreal problems that caused me to build them.",[28,597],{"className":598},[36],[20,600,601],{},"That is usually how my more serious open-source work starts. I repeat something enough times,\nbecome unhappy with the repetition, and eventually the reusable part gets a repository of its\nown.",[28,603],{"className":604},[36],[66,606,608],{"id":607},"where-it-stands-now","Where it stands now",[20,610,611],{},"Celeris is in public beta. The platform, dashboard, SDKs, regional infrastructure, observability,\nand documentation exist, but I am not using that to imply customer adoption, achieved uptime, or\ncommercial scale that I have not recorded.",[28,613],{"className":614},[76],[20,616,617],{},"The next stage is less about adding another long list of features and more about validation:\nrunning a better documented benchmark, exercising failure and recovery paths, tightening the\npublic contract, and learning what real applications need once they stop being test clients.",[28,619],{"className":620},[76],[20,622,623],{},"Celeris began with two practical lessons. Realtime delivery should not force the application\ndatabase onto every message path, and a workaround that succeeds today can still show you the\nsystem you will want tomorrow.",[28,625],{"className":626},[36],{"title":628,"searchDepth":629,"depth":629,"links":630},"",2,[631,632,633,634,635,636,637,638,639,640,641],{"id":68,"depth":629,"text":69},{"id":137,"depth":629,"text":138},{"id":165,"depth":629,"text":166},{"id":290,"depth":629,"text":291},{"id":318,"depth":629,"text":319},{"id":356,"depth":629,"text":357},{"id":398,"depth":629,"text":399},{"id":430,"depth":629,"text":431},{"id":458,"depth":629,"text":459},{"id":555,"depth":629,"text":556},{"id":607,"depth":629,"text":608},"professional","2026-07-29T00:00:00.000Z","A public-beta, binary-first WebSocket platform built in Rust for high-throughput pub/sub across multiple nodes and regions.","md",{},true,"/projects/celeris-realtime",{"title":5,"description":644},"projects/celeris-realtime",[652,653,654,655,656],"Rust","WebSockets","Distributed Systems","Kafka","AWS","cLICf-AYHSFRcj0j7vYdS3rKLESWp1kaf75dYcLD5cM",[659,1621,2593],{"id":660,"title":588,"body":661,"category":642,"date":1608,"description":1609,"extension":645,"image_url":719,"link":694,"meta":1610,"navigation":647,"path":587,"seo":1612,"stem":1613,"tags":1614,"__hash__":1620},"projects/projects/laye.md",{"type":7,"value":662,"toc":1598},[663,667,670,681,684,687,714,720,724,728,746,751,754,758,763,1016,1019,1023,1177,1182,1185,1189,1223,1339,1342,1346,1372,1452,1457,1461,1465,1575,1594],[10,664],{"className":665,"date":666},[13],"2026-05-13",[16,668,588],{"id":669},"laye",[20,671,672,673,676,677,680],{},"A framework-agnostic role and permission based access control library for Rust. Implement the ",[177,674,675],{},"Principal"," trait on your auth type, build composable ",[177,678,679],{},"AccessPolicy"," rules, and wire the provided middleware into actix-web or tower/axum.",[28,682],{"className":683},[76],[24,685],{":tags":686},"[\"Rust\", \"RBAC\", \"Access Control\", \"Library\", \"Actix Web\", \"Axum\"]",[28,688,691,701],{"className":689},[31,32,33,34,35,690],"mb-6",[38,692,696,699],{"className":693,"href":694,"rel":695,"target":49},[44,45,46,47,48],"https://github.com/dev-davexoyinbo/laye",[42],[20,697,698],{},"View on GitHub",[54,700],{"icon":56},[38,702,709,712],{"className":703,"href":707,"rel":708,"target":49},[44,45,46,47,704,705,706],"button-plain","border","border-charcoal-800","https://crates.io/crates/laye",[42],[20,710,711],{},"crates.io",[54,713],{"icon":56},[58,715],{"alt":716,"className":717,"image":719},"A principal passing through a Laye policy gate to a protected route",[62,718],"aspect-[3/2]","/projects/laye/banner.webp",[28,721],{"className":722},[723],"mb-10",[66,725,727],{"id":726},"overview","Overview",[28,729,732],{"className":730},[731],"mb-14",[20,733,734,735,737,738,741,742,745],{},"Laye handles the authorization layer without dictating how you authenticate. You implement the ",[177,736,675],{}," trait on whatever auth struct your application already uses, giving laye access to roles, permissions, and authentication state. From there you compose policies using ",[177,739,740],{},"require_all"," (AND) and ",[177,743,744],{},"require_any"," (OR), and attach the middleware to any route. Unauthenticated requests get a 401; authenticated but unauthorized requests get a 403. The library ships with optional feature flags for actix-web and tower so you only pull in what you need.",[336,747],{"alt":748,"className":749,"image":750},"A Laye policy branching requests into 401, 403, or allowed outcomes",[340],"/projects/laye/authorization-flow.webp",[28,752],{"className":753},[723],[66,755,757],{"id":756},"usage","Usage",[759,760,762],"h3",{"id":761},"implement-principal","Implement Principal",[764,765,769],"pre",{"className":766,"code":767,"language":768,"meta":628,"style":628},"language-rust shiki shiki-themes github-dark","use laye::Principal;\n\n#[derive(Clone)]\nstruct MyUser {\n    roles: Vec\u003CString>,\n    permissions: Vec\u003CString>,\n    authenticated: bool,\n}\n\nimpl Principal for MyUser {\n    fn roles(&self) -> &[String] { &self.roles }\n    fn permissions(&self) -> &[String] { &self.permissions }\n    fn is_authenticated(&self) -> bool { self.authenticated }\n}\n","rust",[177,770,771,793,798,810,822,843,859,873,879,884,900,946,981,1011],{"__ignoreMap":628},[772,773,776,780,784,787,789],"span",{"class":774,"line":775},"line",1,[772,777,779],{"class":778},"snl16","use",[772,781,783],{"class":782},"svObZ"," laye",[772,785,786],{"class":778},"::",[772,788,675],{"class":782},[772,790,792],{"class":791},"s95oV",";\n",[772,794,795],{"class":774,"line":629},[772,796,797],{"emptyLinePlaceholder":647},"\n",[772,799,801,804,807],{"class":774,"line":800},3,[772,802,803],{"class":791},"#[derive(",[772,805,806],{"class":782},"Clone",[772,808,809],{"class":791},")]\n",[772,811,813,816,819],{"class":774,"line":812},4,[772,814,815],{"class":778},"struct",[772,817,818],{"class":782}," MyUser",[772,820,821],{"class":791}," {\n",[772,823,825,828,831,834,837,840],{"class":774,"line":824},5,[772,826,827],{"class":791},"    roles",[772,829,830],{"class":778},":",[772,832,833],{"class":782}," Vec",[772,835,836],{"class":791},"\u003C",[772,838,839],{"class":782},"String",[772,841,842],{"class":791},">,\n",[772,844,846,849,851,853,855,857],{"class":774,"line":845},6,[772,847,848],{"class":791},"    permissions",[772,850,830],{"class":778},[772,852,833],{"class":782},[772,854,836],{"class":791},[772,856,839],{"class":782},[772,858,842],{"class":791},[772,860,862,865,867,870],{"class":774,"line":861},7,[772,863,864],{"class":791},"    authenticated",[772,866,830],{"class":778},[772,868,869],{"class":782}," bool",[772,871,872],{"class":791},",\n",[772,874,876],{"class":774,"line":875},8,[772,877,878],{"class":791},"}\n",[772,880,882],{"class":774,"line":881},9,[772,883,797],{"emptyLinePlaceholder":647},[772,885,887,890,893,896,898],{"class":774,"line":886},10,[772,888,889],{"class":778},"impl",[772,891,892],{"class":782}," Principal",[772,894,895],{"class":778}," for",[772,897,818],{"class":782},[772,899,821],{"class":791},[772,901,903,906,909,912,915,919,922,925,928,931,933,936,938,940,943],{"class":774,"line":902},11,[772,904,905],{"class":778},"    fn",[772,907,908],{"class":782}," roles",[772,910,911],{"class":791},"(",[772,913,914],{"class":778},"&",[772,916,918],{"class":917},"sDLfK","self",[772,920,921],{"class":791},") ",[772,923,924],{"class":778},"->",[772,926,927],{"class":778}," &",[772,929,930],{"class":791},"[",[772,932,839],{"class":782},[772,934,935],{"class":791},"] { ",[772,937,914],{"class":778},[772,939,918],{"class":917},[772,941,942],{"class":778},".",[772,944,945],{"class":791},"roles }\n",[772,947,949,951,954,956,958,960,962,964,966,968,970,972,974,976,978],{"class":774,"line":948},12,[772,950,905],{"class":778},[772,952,953],{"class":782}," permissions",[772,955,911],{"class":791},[772,957,914],{"class":778},[772,959,918],{"class":917},[772,961,921],{"class":791},[772,963,924],{"class":778},[772,965,927],{"class":778},[772,967,930],{"class":791},[772,969,839],{"class":782},[772,971,935],{"class":791},[772,973,914],{"class":778},[772,975,918],{"class":917},[772,977,942],{"class":778},[772,979,980],{"class":791},"permissions }\n",[772,982,984,986,989,991,993,995,997,999,1001,1004,1006,1008],{"class":774,"line":983},13,[772,985,905],{"class":778},[772,987,988],{"class":782}," is_authenticated",[772,990,911],{"class":791},[772,992,914],{"class":778},[772,994,918],{"class":917},[772,996,921],{"class":791},[772,998,924],{"class":778},[772,1000,869],{"class":782},[772,1002,1003],{"class":791}," { ",[772,1005,918],{"class":917},[772,1007,942],{"class":778},[772,1009,1010],{"class":791},"authenticated }\n",[772,1012,1014],{"class":774,"line":1013},14,[772,1015,878],{"class":791},[28,1017],{"className":1018},[36],[759,1020,1022],{"id":1021},"define-a-policy","Define a Policy",[764,1024,1026],{"className":766,"code":1025,"language":768,"meta":628,"style":628},"use laye::{AccessPolicy, AccessRule};\n\nlet policy = AccessPolicy::require_all()\n    .add_rule(AccessRule::Authenticated)\n    .add_policy(\n        AccessPolicy::require_any()\n            .add_rule(AccessRule::Role(\"admin\".into()))\n            .add_rule(AccessRule::Role(\"editor\".into())),\n    );\n",[177,1027,1028,1050,1054,1075,1095,1105,1116,1146,1172],{"__ignoreMap":628},[772,1029,1030,1032,1034,1036,1039,1041,1044,1047],{"class":774,"line":775},[772,1031,779],{"class":778},[772,1033,783],{"class":782},[772,1035,786],{"class":778},[772,1037,1038],{"class":791},"{",[772,1040,679],{"class":782},[772,1042,1043],{"class":791},", ",[772,1045,1046],{"class":782},"AccessRule",[772,1048,1049],{"class":791},"};\n",[772,1051,1052],{"class":774,"line":629},[772,1053,797],{"emptyLinePlaceholder":647},[772,1055,1056,1059,1062,1065,1068,1070,1072],{"class":774,"line":800},[772,1057,1058],{"class":778},"let",[772,1060,1061],{"class":791}," policy ",[772,1063,1064],{"class":778},"=",[772,1066,1067],{"class":782}," AccessPolicy",[772,1069,786],{"class":778},[772,1071,740],{"class":782},[772,1073,1074],{"class":791},"()\n",[772,1076,1077,1080,1083,1085,1087,1089,1092],{"class":774,"line":812},[772,1078,1079],{"class":778},"    .",[772,1081,1082],{"class":782},"add_rule",[772,1084,911],{"class":791},[772,1086,1046],{"class":782},[772,1088,786],{"class":778},[772,1090,1091],{"class":782},"Authenticated",[772,1093,1094],{"class":791},")\n",[772,1096,1097,1099,1102],{"class":774,"line":824},[772,1098,1079],{"class":778},[772,1100,1101],{"class":782},"add_policy",[772,1103,1104],{"class":791},"(\n",[772,1106,1107,1110,1112,1114],{"class":774,"line":845},[772,1108,1109],{"class":782},"        AccessPolicy",[772,1111,786],{"class":778},[772,1113,744],{"class":782},[772,1115,1074],{"class":791},[772,1117,1118,1121,1123,1125,1127,1129,1132,1134,1138,1140,1143],{"class":774,"line":861},[772,1119,1120],{"class":778},"            .",[772,1122,1082],{"class":782},[772,1124,911],{"class":791},[772,1126,1046],{"class":782},[772,1128,786],{"class":778},[772,1130,1131],{"class":782},"Role",[772,1133,911],{"class":791},[772,1135,1137],{"class":1136},"sU2Wk","\"admin\"",[772,1139,942],{"class":778},[772,1141,1142],{"class":782},"into",[772,1144,1145],{"class":791},"()))\n",[772,1147,1148,1150,1152,1154,1156,1158,1160,1162,1165,1167,1169],{"class":774,"line":875},[772,1149,1120],{"class":778},[772,1151,1082],{"class":782},[772,1153,911],{"class":791},[772,1155,1046],{"class":782},[772,1157,786],{"class":778},[772,1159,1131],{"class":782},[772,1161,911],{"class":791},[772,1163,1164],{"class":1136},"\"editor\"",[772,1166,942],{"class":778},[772,1168,1142],{"class":782},[772,1170,1171],{"class":791},"())),\n",[772,1173,1174],{"class":774,"line":881},[772,1175,1176],{"class":791},"    );\n",[336,1178],{"alt":1179,"className":1180,"image":1181},"Nested all-rules and any-rule access policies",[340],"/projects/laye/policy-composition.webp",[28,1183],{"className":1184},[36],[759,1186,1188],{"id":1187},"wire-into-actix-web","Wire into actix-web",[764,1190,1194],{"className":1191,"code":1192,"language":1193,"meta":628,"style":628},"language-toml shiki shiki-themes github-dark","[dependencies]\nlaye = { version = \"0.1\", features = [\"actix-web\"] }\n","toml",[177,1195,1196,1206],{"__ignoreMap":628},[772,1197,1198,1200,1203],{"class":774,"line":775},[772,1199,930],{"class":791},[772,1201,1202],{"class":782},"dependencies",[772,1204,1205],{"class":791},"]\n",[772,1207,1208,1211,1214,1217,1220],{"class":774,"line":629},[772,1209,1210],{"class":791},"laye = { version = ",[772,1212,1213],{"class":1136},"\"0.1\"",[772,1215,1216],{"class":791},", features = [",[772,1218,1219],{"class":1136},"\"actix-web\"",[772,1221,1222],{"class":791},"] }\n",[764,1224,1226],{"className":766,"code":1225,"language":768,"meta":628,"style":628},"use laye::actix::PolicyMiddlewareFactory;\n\nApp::new()\n    .service(\n        web::resource(\"/admin\")\n            .wrap(PolicyMiddlewareFactory::new(policy))\n            .route(web::get().to(handler)),\n    )\n",[177,1227,1228,1246,1250,1262,1271,1288,1306,1334],{"__ignoreMap":628},[772,1229,1230,1232,1234,1236,1239,1241,1244],{"class":774,"line":775},[772,1231,779],{"class":778},[772,1233,783],{"class":782},[772,1235,786],{"class":778},[772,1237,1238],{"class":782},"actix",[772,1240,786],{"class":778},[772,1242,1243],{"class":782},"PolicyMiddlewareFactory",[772,1245,792],{"class":791},[772,1247,1248],{"class":774,"line":629},[772,1249,797],{"emptyLinePlaceholder":647},[772,1251,1252,1255,1257,1260],{"class":774,"line":800},[772,1253,1254],{"class":782},"App",[772,1256,786],{"class":778},[772,1258,1259],{"class":782},"new",[772,1261,1074],{"class":791},[772,1263,1264,1266,1269],{"class":774,"line":812},[772,1265,1079],{"class":778},[772,1267,1268],{"class":782},"service",[772,1270,1104],{"class":791},[772,1272,1273,1276,1278,1281,1283,1286],{"class":774,"line":824},[772,1274,1275],{"class":782},"        web",[772,1277,786],{"class":778},[772,1279,1280],{"class":782},"resource",[772,1282,911],{"class":791},[772,1284,1285],{"class":1136},"\"/admin\"",[772,1287,1094],{"class":791},[772,1289,1290,1292,1295,1297,1299,1301,1303],{"class":774,"line":845},[772,1291,1120],{"class":778},[772,1293,1294],{"class":782},"wrap",[772,1296,911],{"class":791},[772,1298,1243],{"class":782},[772,1300,786],{"class":778},[772,1302,1259],{"class":782},[772,1304,1305],{"class":791},"(policy))\n",[772,1307,1308,1310,1313,1315,1318,1320,1323,1326,1328,1331],{"class":774,"line":861},[772,1309,1120],{"class":778},[772,1311,1312],{"class":782},"route",[772,1314,911],{"class":791},[772,1316,1317],{"class":782},"web",[772,1319,786],{"class":778},[772,1321,1322],{"class":782},"get",[772,1324,1325],{"class":791},"()",[772,1327,942],{"class":778},[772,1329,1330],{"class":782},"to",[772,1332,1333],{"class":791},"(handler)),\n",[772,1335,1336],{"class":774,"line":875},[772,1337,1338],{"class":791},"    )\n",[28,1340],{"className":1341},[36],[759,1343,1345],{"id":1344},"wire-into-axumtower","Wire into axum/tower",[764,1347,1349],{"className":1191,"code":1348,"language":1193,"meta":628,"style":628},"[dependencies]\nlaye = { version = \"0.1\", features = [\"tower\"] }\n",[177,1350,1351,1359],{"__ignoreMap":628},[772,1352,1353,1355,1357],{"class":774,"line":775},[772,1354,930],{"class":791},[772,1356,1202],{"class":782},[772,1358,1205],{"class":791},[772,1360,1361,1363,1365,1367,1370],{"class":774,"line":629},[772,1362,1210],{"class":791},[772,1364,1213],{"class":1136},[772,1366,1216],{"class":791},[772,1368,1369],{"class":1136},"\"tower\"",[772,1371,1222],{"class":791},[764,1373,1375],{"className":766,"code":1374,"language":768,"meta":628,"style":628},"use laye::tower::AccessControlLayer;\n\nlet app = Router::new()\n    .route(\"/admin\", get(handler))\n    .layer(AccessControlLayer::new(policy));\n",[177,1376,1377,1395,1399,1417,1434],{"__ignoreMap":628},[772,1378,1379,1381,1383,1385,1388,1390,1393],{"class":774,"line":775},[772,1380,779],{"class":778},[772,1382,783],{"class":782},[772,1384,786],{"class":778},[772,1386,1387],{"class":782},"tower",[772,1389,786],{"class":778},[772,1391,1392],{"class":782},"AccessControlLayer",[772,1394,792],{"class":791},[772,1396,1397],{"class":774,"line":629},[772,1398,797],{"emptyLinePlaceholder":647},[772,1400,1401,1403,1406,1408,1411,1413,1415],{"class":774,"line":800},[772,1402,1058],{"class":778},[772,1404,1405],{"class":791}," app ",[772,1407,1064],{"class":778},[772,1409,1410],{"class":782}," Router",[772,1412,786],{"class":778},[772,1414,1259],{"class":782},[772,1416,1074],{"class":791},[772,1418,1419,1421,1423,1425,1427,1429,1431],{"class":774,"line":812},[772,1420,1079],{"class":778},[772,1422,1312],{"class":782},[772,1424,911],{"class":791},[772,1426,1285],{"class":1136},[772,1428,1043],{"class":791},[772,1430,1322],{"class":782},[772,1432,1433],{"class":791},"(handler))\n",[772,1435,1436,1438,1441,1443,1445,1447,1449],{"class":774,"line":824},[772,1437,1079],{"class":778},[772,1439,1440],{"class":782},"layer",[772,1442,911],{"class":791},[772,1444,1392],{"class":782},[772,1446,786],{"class":778},[772,1448,1259],{"class":782},[772,1450,1451],{"class":791},"(policy));\n",[336,1453],{"alt":1454,"className":1455,"image":1456},"Laye core adapting policies to actix-web and tower or axum",[340],"/projects/laye/framework-adapters.webp",[28,1458],{"className":1459},[1460],"mb-12",[66,1462,1464],{"id":1463},"highlights","Highlights",[28,1466,1473,1504,1529,1550],{"className":1467},[1468,1469,1470,1471,1472],"grid","grid-cols-2","sm:grid-cols-4","gap-4","md:gap-6",[1474,1475,1478,1482],"content-card",{"className":1476},[1477],"col-span-2",[759,1479,1481],{"id":1480},"policy-dsl","Policy DSL",[28,1483,1487],{"className":1484},[1485,1486],"text-xs","text-dune-600",[1488,1489,1490,1496,1501],"ul",{},[1491,1492,1493,1495],"li",{},[177,1494,740],{}," for AND logic across rules and nested policies",[1491,1497,1498,1500],{},[177,1499,744],{}," for OR logic",[1491,1502,1503],{},"Unlimited nesting depth for complex access scenarios",[1474,1505,1507,1511],{"className":1506},[1477],[759,1508,1510],{"id":1509},"framework-support","Framework Support",[28,1512,1514],{"className":1513},[1485,1486],[1488,1515,1516,1521,1526],{},[1491,1517,1518,1519],{},"actix-web middleware via ",[177,1520,1243],{},[1491,1522,1523,1524],{},"tower/axum middleware via ",[177,1525,1392],{},[1491,1527,1528],{},"Feature flags keep unused framework code out of your binary",[1474,1530,1532,1536],{"className":1531},[1477],[759,1533,1535],{"id":1534},"http-responses","HTTP Responses",[28,1537,1539],{"className":1538},[1485,1486],[1488,1540,1541,1544,1547],{},[1491,1542,1543],{},"401 Unauthorized for requests with no authenticated principal",[1491,1545,1546],{},"403 Forbidden for authenticated but policy-failing requests",[1491,1548,1549],{},"No manual status code handling needed in your handlers",[1474,1551,1553,1557],{"className":1552},[1477],[759,1554,1556],{"id":1555},"ergonomics","Ergonomics",[28,1558,1560],{"className":1559},[1485,1486],[1488,1561,1562,1565,1568],{},[1491,1563,1564],{},"Zero coupling to your auth implementation",[1491,1566,1567],{},"Bring your own user struct, roles, and permissions",[1491,1569,1570,1571,1574],{},"Minimal install: ",[177,1572,1573],{},"laye = \"0.1\""," for the core with no framework overhead",[28,1576,1579,1587],{"className":1577},[31,32,33,34,1578],"mt-8",[38,1580,1583,1585],{"className":1581,"href":694,"rel":1582,"target":49},[44,45,46,47,48],[42],[20,1584,698],{},[54,1586],{"icon":56},[38,1588,1591],{"className":1589,"href":1590},[44,45,46,47,704,705,706],"/projects",[20,1592,1593],{},"Back to Projects",[1595,1596,1597],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":628,"searchDepth":629,"depth":629,"links":1599},[1600,1601,1607],{"id":726,"depth":629,"text":727},{"id":756,"depth":629,"text":757,"children":1602},[1603,1604,1605,1606],{"id":761,"depth":800,"text":762},{"id":1021,"depth":800,"text":1022},{"id":1187,"depth":800,"text":1188},{"id":1344,"depth":800,"text":1345},{"id":1463,"depth":629,"text":1464},"2026-05-13T00:00:00.000Z","Framework-agnostic RBAC library for Rust with composable AccessPolicy rules and plug-and-play middleware for actix-web and tower/axum.",{"license":1611},"MIT",{"title":588,"description":1609},"projects/laye",[652,1615,1616,1617,1618,1619],"RBAC","Access Control","Library","Actix Web","Axum","CBs1-0cGYl4dChpXPYinH8vvdeOrEKra1bNiBPX8wTA",{"id":1622,"title":578,"body":1623,"category":642,"date":2585,"description":2586,"extension":645,"image_url":1687,"link":1645,"meta":2587,"navigation":647,"path":577,"seo":2588,"stem":2589,"tags":2590,"__hash__":2592},"projects/projects/distkit.md",{"type":7,"value":1624,"toc":2576},[1625,1629,1631,1634,1637,1640,1682,1688,1691,1695,1709,1715,1753,1758,1761,1765,1780,1943,1955,1960,1963,1967,1978,1984,2002,2007,2156,2159,2163,2189,2199,2345,2350,2353,2357,2396,2413,2418,2421,2423,2533,2573],[10,1626],{"className":1627,"date":1628},[13],"2026-04-03",[16,1630,578],{"id":578},[20,1632,1633],{},"Distkit (DISTributed systems KIT) is a Rust library of the small building blocks you keep reaching for once an application outgrows a single server. It started as a distributed counter I needed for a realtime platform, and over time it grew into a handful of primitives that all share the same backend: Redis. As of the latest release it ships counters (strict and lax), instance-aware counters that clean up after dead servers on their own, distributed locks, and sliding-window rate limiting.",[28,1635],{"className":1636},[690],[24,1638],{":tags":1639},"[\"Rust\", \"Redis\", \"Distributed Systems\", \"Library\", \"Async\"]",[28,1641,1643,1653,1663,1672],{"className":1642},[31,32,33,34,35,690],[38,1644,1648,1651],{"href":1645,"rel":1646,"className":1647,"target":49},"https://github.com/dev-davexoyinbo/distkit",[42],[44,45,46,47,48],[20,1649,1650],{},"GitHub Repo",[54,1652],{"icon":56},[38,1654,1658,1661],{"href":1655,"rel":1656,"className":1657,"target":49},"https://distkit.davidoyinbo.com/",[42],[44,45,46,47,704,705,706],[20,1659,1660],{},"Documentation Site",[54,1662],{"icon":56},[38,1664,1668,1670],{"href":1665,"rel":1666,"className":1667,"target":49},"https://crates.io/crates/distkit",[42],[44,45,46,47,704,705,706],[20,1669,711],{},[54,1671],{"icon":56},[38,1673,1677,1680],{"href":1674,"rel":1675,"className":1676,"target":49},"https://docs.rs/distkit",[42],[44,45,46,47,704,705,706],[20,1678,1679],{},"API Docs (docs.rs)",[54,1681],{"icon":56},[58,1683],{"alt":1684,"className":1685,"image":1687},"Distkit connecting counters, locks, and rate limits through a shared toolkit",[62,1686],"aspect-video","/projects/distkit/banner.webp",[28,1689],{"className":1690},[723],[66,1692,1694],{"id":1693},"the-idea","The idea",[28,1696,1698],{"className":1697},[690],[20,1699,1700,1701,1704,1705,1708],{},"Every primitive in distkit solves a problem that is easy on one machine and surprisingly fiddly the moment there are two. The whole library leans on one fact: Redis runs commands one at a time. That single-threadedness is what lets a counter increment, a lock acquisition, or a rate-limit check happen atomically across an entire fleet. The work each type does is wrapped in a Lua script so the read and the write travel together, leaving no gap for a second server to slip into. Everything is async, built on ",[177,1702,1703],{},"redis::aio::ConnectionManager",", and the whole crate is ",[177,1706,1707],{},"#![forbid(unsafe_code)]"," with no panics in library paths.",[28,1710,1712],{"className":1711},[690],[20,1713,1714],{},"The pieces are gated behind feature flags so you only pull in what you use. Counters are on by default; instance-aware counters, locks, and rate limiting are each opt-in.",[764,1716,1718],{"className":1191,"code":1717,"language":1193,"meta":628,"style":628},"[dependencies]\ndistkit = { version = \"0.5\", features = [\"instance-aware-counter\", \"lock\", \"trypema\"] }\n",[177,1719,1720,1728],{"__ignoreMap":628},[772,1721,1722,1724,1726],{"class":774,"line":775},[772,1723,930],{"class":791},[772,1725,1202],{"class":782},[772,1727,1205],{"class":791},[772,1729,1730,1733,1736,1738,1741,1743,1746,1748,1751],{"class":774,"line":629},[772,1731,1732],{"class":791},"distkit = { version = ",[772,1734,1735],{"class":1136},"\"0.5\"",[772,1737,1216],{"class":791},[772,1739,1740],{"class":1136},"\"instance-aware-counter\"",[772,1742,1043],{"class":791},[772,1744,1745],{"class":1136},"\"lock\"",[772,1747,1043],{"class":791},[772,1749,1750],{"class":1136},"\"trypema\"",[772,1752,1222],{"class":791},[336,1754],{"alt":1755,"className":1756,"image":1757},"A request and Lua script producing one atomic result",[340],"/projects/distkit/atomic-primitives.webp",[28,1759],{"className":1760},[731],[66,1762,1764],{"id":1763},"counters","Counters",[28,1766,1768],{"className":1767},[690],[20,1769,1770,1771,1775,1776,1779],{},"The counters come in two flavours, and the choice between them is really a choice about what you are willing to trade. A ",[1772,1773,1774],"strong",{},"StrictCounter"," does a Redis round-trip on every operation, so a read always reflects the latest write. It is the right tool when being wrong is expensive: billing, inventory, seat counts. A ",[1772,1777,1778],{},"LaxCounter"," buffers increments in memory and flushes them to Redis on a short interval (around 20 ms by default). The hot path becomes an in-memory atomic add that finishes in well under a microsecond, at the cost of a small window where servers can disagree on the total. For analytics and high-volume metrics that trade is almost always worth it.",[764,1781,1783],{"className":766,"code":1782,"language":768,"meta":628,"style":628},"use distkit::{DistkitRedisKey, counter::{StrictCounter, CounterOptions, CounterTrait}};\n\nlet counter = StrictCounter::new(CounterOptions::new(prefix, conn));\nlet key = DistkitRedisKey::try_from(\"orders\".to_string())?;\ncounter.inc(&key, 1).await?;\nlet total = counter.get(&key).await?;\n",[177,1784,1785,1823,1827,1854,1889,1916],{"__ignoreMap":628},[772,1786,1787,1789,1792,1794,1796,1799,1801,1804,1806,1808,1810,1812,1815,1817,1820],{"class":774,"line":775},[772,1788,779],{"class":778},[772,1790,1791],{"class":782}," distkit",[772,1793,786],{"class":778},[772,1795,1038],{"class":791},[772,1797,1798],{"class":782},"DistkitRedisKey",[772,1800,1043],{"class":791},[772,1802,1803],{"class":782},"counter",[772,1805,786],{"class":778},[772,1807,1038],{"class":791},[772,1809,1774],{"class":782},[772,1811,1043],{"class":791},[772,1813,1814],{"class":782},"CounterOptions",[772,1816,1043],{"class":791},[772,1818,1819],{"class":782},"CounterTrait",[772,1821,1822],{"class":791},"}};\n",[772,1824,1825],{"class":774,"line":629},[772,1826,797],{"emptyLinePlaceholder":647},[772,1828,1829,1831,1834,1836,1839,1841,1843,1845,1847,1849,1851],{"class":774,"line":800},[772,1830,1058],{"class":778},[772,1832,1833],{"class":791}," counter ",[772,1835,1064],{"class":778},[772,1837,1838],{"class":782}," StrictCounter",[772,1840,786],{"class":778},[772,1842,1259],{"class":782},[772,1844,911],{"class":791},[772,1846,1814],{"class":782},[772,1848,786],{"class":778},[772,1850,1259],{"class":782},[772,1852,1853],{"class":791},"(prefix, conn));\n",[772,1855,1856,1858,1861,1863,1866,1868,1871,1873,1876,1878,1881,1884,1887],{"class":774,"line":812},[772,1857,1058],{"class":778},[772,1859,1860],{"class":791}," key ",[772,1862,1064],{"class":778},[772,1864,1865],{"class":782}," DistkitRedisKey",[772,1867,786],{"class":778},[772,1869,1870],{"class":782},"try_from",[772,1872,911],{"class":791},[772,1874,1875],{"class":1136},"\"orders\"",[772,1877,942],{"class":778},[772,1879,1880],{"class":782},"to_string",[772,1882,1883],{"class":791},"())",[772,1885,1886],{"class":778},"?",[772,1888,792],{"class":791},[772,1890,1891,1893,1895,1898,1900,1902,1905,1908,1911,1914],{"class":774,"line":824},[772,1892,1803],{"class":791},[772,1894,942],{"class":778},[772,1896,1897],{"class":782},"inc",[772,1899,911],{"class":791},[772,1901,914],{"class":778},[772,1903,1904],{"class":791},"key, ",[772,1906,1907],{"class":917},"1",[772,1909,1910],{"class":791},")",[772,1912,1913],{"class":778},".await?",[772,1915,792],{"class":791},[772,1917,1918,1920,1923,1925,1928,1930,1932,1934,1936,1939,1941],{"class":774,"line":845},[772,1919,1058],{"class":778},[772,1921,1922],{"class":791}," total ",[772,1924,1064],{"class":778},[772,1926,1927],{"class":791}," counter",[772,1929,942],{"class":778},[772,1931,1322],{"class":782},[772,1933,911],{"class":791},[772,1935,914],{"class":778},[772,1937,1938],{"class":791},"key)",[772,1940,1913],{"class":778},[772,1942,792],{"class":791},[28,1944,1946],{"className":1945},[690],[20,1947,1948,1949,1951,1952,1954],{},"Both also support conditional writes, where an increment or a set only goes through if the current value passes a comparison, and batched operations that preserve input order. Swapping ",[177,1950,1774],{}," for ",[177,1953,1778],{}," is a one-line change; the interface is the same.",[336,1956],{"alt":1957,"className":1958,"image":1959},"Strict and lax counter paths trading consistency for throughput",[340],"/projects/distkit/counter-models.webp",[28,1961],{"className":1962},[723],[759,1964,1966],{"id":1965},"instance-aware-counters","Instance-aware counters",[28,1968,1970],{"className":1969},[690],[20,1971,1972,1973,1977],{},"A plain global counter falls apart for one specific question: how many of ",[1974,1975,1976],"em",{},"something"," are alive across the cluster right now, like open WebSocket connections. Increment on connect, decrement on disconnect, and it works fine until a server crashes with five hundred connections still open. Nobody runs those five hundred decrements, and the total stays wrong forever.",[28,1979,1981],{"className":1980},[690],[20,1982,1983],{},"The instance-aware counters fix this by giving every running process its own slice of the total. Each one is the sole writer of its slice, so there is nothing to coordinate, and the cumulative is just the sum of the live slices. Liveness rides along on ordinary work: every operation an instance runs also stamps a heartbeat. When a process goes quiet for longer than the dead-instance threshold (thirty seconds by default), the next instance to touch that counter subtracts the dead one's contribution and removes it. The cleanup is a side effect of normal traffic rather than a background job, so a crashed server's count simply evaporates the next time anyone looks. If the server was only briefly unreachable and comes back, its slice is recovered and resynchronised.",[28,1985,1987],{"className":1986},[690],[20,1988,1989,1990,1993,1994,1997,1998,2001],{},"There is also a notion of an ",[1772,1991,1992],{},"epoch"," per key, essentially a version number for the counter's current era. A global ",[177,1995,1996],{},"set"," or ",[177,1999,2000],{},"del"," bumps the epoch, which tells any stale instance to reset its stored slice before contributing again. That is what keeps a coordinated reset from double-counting. As with the plain counters, instance-aware comes in strict and lax variants. Reads return a pair: the cluster-wide cumulative and this instance's own slice.",[336,2003],{"alt":2004,"className":2005,"image":2006},"Live instance slices, heartbeats, and dead-instance cleanup",[340],"/projects/distkit/instance-aware.webp",[185,2008,2009,2025],{},[188,2010,2011],{},[191,2012,2013,2015,2017,2019,2022],{},[194,2014],{},[194,2016,1774],{},[194,2018,1778],{},[194,2020,2021],{},"StrictInstanceAware",[194,2023,2024],{},"LaxInstanceAware",[208,2026,2027,2044,2063,2080,2097,2112,2137],{},[191,2028,2029,2034,2037,2040,2042],{},[213,2030,2031],{},[1772,2032,2033],{},"Consistency",[213,2035,2036],{},"Immediate",[213,2038,2039],{},"Eventual",[213,2041,2036],{},[213,2043,2039],{},[191,2045,2046,2053,2056,2059,2061],{},[213,2047,2048],{},[1772,2049,2050,2052],{},[177,2051,1897],{}," latency",[213,2054,2055],{},"Redis round-trip",[213,2057,2058],{},"Sub-microsecond",[213,2060,2055],{},[213,2062,2058],{},[191,2064,2065,2070,2073,2076,2078],{},[213,2066,2067],{},[1772,2068,2069],{},"Redis I/O",[213,2071,2072],{},"Every call",[213,2074,2075],{},"Batched",[213,2077,2072],{},[213,2079,2075],{},[191,2081,2082,2087,2090,2092,2095],{},[213,2083,2084],{},[1772,2085,2086],{},"Per-instance tracking",[213,2088,2089],{},"No",[213,2091,2089],{},[213,2093,2094],{},"Yes",[213,2096,2094],{},[191,2098,2099,2104,2106,2108,2110],{},[213,2100,2101],{},[1772,2102,2103],{},"Dead-instance cleanup",[213,2105,2089],{},[213,2107,2089],{},[213,2109,2094],{},[213,2111,2094],{},[191,2113,2114,2119,2124,2128,2133],{},[213,2115,2116],{},[1772,2117,2118],{},"Feature flag",[213,2120,2121,2123],{},[177,2122,1803],{}," (default)",[213,2125,2126,2123],{},[177,2127,1803],{},[213,2129,2130],{},[177,2131,2132],{},"instance-aware-counter",[213,2134,2135],{},[177,2136,2132],{},[191,2138,2139,2144,2147,2150,2153],{},[213,2140,2141],{},[1772,2142,2143],{},"Use case",[213,2145,2146],{},"Billing, inventory",[213,2148,2149],{},"High-throughput analytics",[213,2151,2152],{},"Live connection counts",[213,2154,2155],{},"High-frequency per-node metrics",[28,2157],{"className":2158},[731],[66,2160,2162],{"id":2161},"distributed-locks","Distributed locks",[28,2164,2166],{"className":2165},[690],[20,2167,2168,2169,2172,2173,2176,2177,2180,2181,2184,2185,2188],{},"The newest addition is a pair of distributed locks, behind the ",[177,2170,2171],{},"lock"," feature. There is a ",[177,2174,2175],{},"Mutex"," for plain mutual exclusion and a writer-preferring ",[177,2178,2179],{},"RwLock"," for the many-readers-or-one-writer case. Both are deliberately modelled on ",[177,2182,2183],{},"tokio::sync::Mutex"," and ",[177,2186,2187],{},"tokio::sync::RwLock",", so if you have used those, these will feel familiar. The one conceptual difference is that the guards hold no data; they are pure access tokens that say \"you currently hold this lock.\"",[28,2190,2192],{"className":2191},[690],[20,2193,2194,2195,2198],{},"A held lock keeps itself alive by refreshing its lease in the background (every third of the TTL), and it releases automatically when the guard is dropped. For callers who want to observe the final release rather than let it happen silently, there is an awaitable ",[177,2196,2197],{},"release()",". Each lock offers three ways to acquire it: wait until it is free, try once and give up immediately, or wait up to a bounded timeout. TTL, maximum wait, retry interval, owner id, and namespace are all configurable.",[764,2200,2202],{"className":766,"code":2201,"language":768,"meta":628,"style":628},"use distkit::{DistkitRedisKey, lock::{Mutex, LockOptions}};\n\nlet key = DistkitRedisKey::try_from(\"invoice_42\".to_string())?;\nlet mutex = Mutex::new(LockOptions::new(key, conn));\n\nlet guard = mutex.lock().await?; // waits until acquired\n// ... critical section ...\nguard.release().await?;\n",[177,2203,2204,2233,2237,2266,2293,2297,2324,2329],{"__ignoreMap":628},[772,2205,2206,2208,2210,2212,2214,2216,2218,2220,2222,2224,2226,2228,2231],{"class":774,"line":775},[772,2207,779],{"class":778},[772,2209,1791],{"class":782},[772,2211,786],{"class":778},[772,2213,1038],{"class":791},[772,2215,1798],{"class":782},[772,2217,1043],{"class":791},[772,2219,2171],{"class":782},[772,2221,786],{"class":778},[772,2223,1038],{"class":791},[772,2225,2175],{"class":782},[772,2227,1043],{"class":791},[772,2229,2230],{"class":782},"LockOptions",[772,2232,1822],{"class":791},[772,2234,2235],{"class":774,"line":629},[772,2236,797],{"emptyLinePlaceholder":647},[772,2238,2239,2241,2243,2245,2247,2249,2251,2253,2256,2258,2260,2262,2264],{"class":774,"line":800},[772,2240,1058],{"class":778},[772,2242,1860],{"class":791},[772,2244,1064],{"class":778},[772,2246,1865],{"class":782},[772,2248,786],{"class":778},[772,2250,1870],{"class":782},[772,2252,911],{"class":791},[772,2254,2255],{"class":1136},"\"invoice_42\"",[772,2257,942],{"class":778},[772,2259,1880],{"class":782},[772,2261,1883],{"class":791},[772,2263,1886],{"class":778},[772,2265,792],{"class":791},[772,2267,2268,2270,2273,2275,2278,2280,2282,2284,2286,2288,2290],{"class":774,"line":812},[772,2269,1058],{"class":778},[772,2271,2272],{"class":791}," mutex ",[772,2274,1064],{"class":778},[772,2276,2277],{"class":782}," Mutex",[772,2279,786],{"class":778},[772,2281,1259],{"class":782},[772,2283,911],{"class":791},[772,2285,2230],{"class":782},[772,2287,786],{"class":778},[772,2289,1259],{"class":782},[772,2291,2292],{"class":791},"(key, conn));\n",[772,2294,2295],{"class":774,"line":824},[772,2296,797],{"emptyLinePlaceholder":647},[772,2298,2299,2301,2304,2306,2309,2311,2313,2315,2317,2320],{"class":774,"line":845},[772,2300,1058],{"class":778},[772,2302,2303],{"class":791}," guard ",[772,2305,1064],{"class":778},[772,2307,2308],{"class":791}," mutex",[772,2310,942],{"class":778},[772,2312,2171],{"class":782},[772,2314,1325],{"class":791},[772,2316,1913],{"class":778},[772,2318,2319],{"class":791},"; ",[772,2321,2323],{"class":2322},"sAwPA","// waits until acquired\n",[772,2325,2326],{"class":774,"line":861},[772,2327,2328],{"class":2322},"// ... critical section ...\n",[772,2330,2331,2334,2336,2339,2341,2343],{"class":774,"line":875},[772,2332,2333],{"class":791},"guard",[772,2335,942],{"class":778},[772,2337,2338],{"class":782},"release",[772,2340,1325],{"class":791},[772,2342,1913],{"class":778},[772,2344,792],{"class":791},[336,2346],{"alt":2347,"className":2348,"image":2349},"Distributed mutex and read-write locks with renewable leases",[340],"/projects/distkit/distributed-locks.webp",[28,2351],{"className":2352},[731],[66,2354,2356],{"id":2355},"rate-limiting","Rate limiting",[28,2358,2360],{"className":2359},[690],[20,2361,2362,2363,2366,2367,2371,2372,2375,2376,2379,2380,2383,2384,2387,2388,2391,2392,2395],{},"The ",[177,2364,2365],{},"trypema"," feature re-exports my ",[38,2368,2365],{"href":2369,"rel":2370},"https://davidoyinbo.com/projects/trypema-rate-limiter",[42]," crate under ",[177,2373,2374],{},"distkit::trypema",", so distkit can also do sliding-window rate limiting without pulling in a separate dependency. There are three providers depending on how far the enforcement needs to reach: ",[1772,2377,2378],{},"local"," for in-process limiting, ",[1772,2381,2382],{},"Redis"," for distributed enforcement across servers, and ",[1772,2385,2386],{},"hybrid"," for a local fast-path that periodically syncs with Redis. Each supports an ",[1772,2389,2390],{},"absolute"," strategy (a straight allow/reject) and a ",[1772,2393,2394],{},"suppressed"," one (a probabilistic ramp that starts shedding load before you hit the wall).",[28,2397,2399],{"className":2398},[690],[20,2400,2401,2402,2407,2408,942],{},"For the full surface there is the ",[38,2403,2406],{"href":2404,"rel":2405},"https://trypema.davidoyinbo.com/",[42],"trypema documentation site"," and the ",[38,2409,2412],{"href":2410,"rel":2411},"https://docs.rs/trypema/",[42],"Rust docs",[336,2414],{"alt":2415,"className":2416,"image":2417},"Local, Redis, and hybrid rate-limiter provider topologies",[340],"/projects/trypema-rate-limiter/provider-topologies.webp",[28,2419],{"className":2420},[731],[66,2422,1464],{"id":1463},[28,2424,2426,2459,2483,2507],{"className":2425},[1468,1469,1470,1471,1472],[1474,2427,2429,2433],{"className":2428},[1477],[759,2430,2432],{"id":2431},"whats-in-the-box","What's in the box",[28,2434,2436],{"className":2435},[1485,1486],[1488,2437,2438,2441,2444,2451,2454],{},[1491,2439,2440],{},"Strict and lax distributed counters",[1491,2442,2443],{},"Instance-aware counters with per-instance slices and epochs",[1491,2445,2446,2448,2449],{},[177,2447,2175],{}," and writer-preferring ",[177,2450,2179],{},[1491,2452,2453],{},"Sliding-window rate limiting via trypema",[1491,2455,2456,2457],{},"Everything async over ",[177,2458,1703],{},[1474,2460,2462,2466],{"className":2461},[1477],[759,2463,2465],{"id":2464},"consistency-model","Consistency model",[28,2467,2469],{"className":2468},[1485,1486],[1488,2470,2471,2474,2477,2480],{},[1491,2472,2473],{},"Strict types: immediately consistent, no local state to go stale",[1491,2475,2476],{},"Lax types: eventual consistency, configurable lag (default ~20 ms)",[1491,2478,2479],{},"Atomic Lua scripts keep every operation a single serialised step",[1491,2481,2482],{},"Per-key epochs prevent double-counting after a global reset",[1474,2484,2486,2490],{"className":2485},[1477],[759,2487,2489],{"id":2488},"self-healing","Self-healing",[28,2491,2493],{"className":2492},[1485,1486],[1488,2494,2495,2498,2501,2504],{},[1491,2496,2497],{},"Heartbeats ride along on ordinary operations, no separate ticker",[1491,2499,2500],{},"Dead instances are cleaned up by surviving instances, not a background job",[1491,2502,2503],{},"Briefly-unreachable instances recover and resync their slice",[1491,2505,2506],{},"Locks refresh their lease automatically and release on drop",[1474,2508,2510,2514],{"className":2509},[1477],[759,2511,2513],{"id":2512},"built-to-be-trusted","Built to be trusted",[28,2515,2517],{"className":2516},[1485,1486],[1488,2518,2519,2524,2527,2530],{},[1491,2520,2521,2523],{},[177,2522,1707],{},": no unsafe blocks anywhere in the library",[1491,2525,2526],{},"No panics in library code; fallible paths return a typed error",[1491,2528,2529],{},"Runnable doc-test examples, each isolated to its own Redis prefix",[1491,2531,2532],{},"Criterion benchmarks across every counter type and the locks",[28,2534,2536,2544,2552,2560,2568],{"className":2535},[31,32,33,34,1578],[38,2537,2540,2542],{"href":1645,"rel":2538,"className":2539,"target":49},[42],[44,45,46,47,48],[20,2541,1650],{},[54,2543],{"icon":56},[38,2545,2548,2550],{"href":1655,"rel":2546,"className":2547,"target":49},[42],[44,45,46,47,704,705,706],[20,2549,1660],{},[54,2551],{"icon":56},[38,2553,2556,2558],{"href":1665,"rel":2554,"className":2555,"target":49},[42],[44,45,46,47,704,705,706],[20,2557,711],{},[54,2559],{"icon":56},[38,2561,2564,2566],{"href":1674,"rel":2562,"className":2563,"target":49},[42],[44,45,46,47,704,705,706],[20,2565,1679],{},[54,2567],{"icon":56},[38,2569,2571],{"href":1590,"className":2570},[44,45,46,47,704,705,706],[20,2572,1593],{},[1595,2574,2575],{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":628,"searchDepth":629,"depth":629,"links":2577},[2578,2579,2582,2583,2584],{"id":1693,"depth":629,"text":1694},{"id":1763,"depth":629,"text":1764,"children":2580},[2581],{"id":1965,"depth":800,"text":1966},{"id":2161,"depth":629,"text":2162},{"id":2355,"depth":629,"text":2356},{"id":1463,"depth":629,"text":1464},"2026-04-03T00:00:00.000Z","A Rust toolkit of distributed systems primitives backed by Redis. Strict and lax counters, instance-aware counters with automatic dead-instance cleanup, distributed locks (Mutex and RwLock), and sliding-window rate limiting.",{"license":1611},{"title":578,"description":2586},"projects/distkit",[652,2382,654,1617,2591],"Async","vApXAfYcOMMQBp_Qyu1K5s9XfwI1RARoGfXm7GJs8Pw",{"id":2594,"title":2595,"body":2596,"category":642,"date":3415,"description":3416,"extension":645,"image_url":2417,"link":2404,"meta":3417,"navigation":647,"path":567,"seo":3418,"stem":3419,"tags":3420,"__hash__":3422},"projects/projects/trypema-rate-limiter.md","Trypema Rate Limiter",{"type":7,"value":2597,"toc":3406},[2598,2602,2605,2608,2611,2614,2617,2620,2658,2662,2666,2715,2719,2730,2733,2737,2755,2758,2768,2773,2777,2783,2786,2803,2808,2812,2833,2836,3140,3143,3161,3164,3168,3235,3238,3252,3257,3261,3264,3267,3278,3283,3306,3310,3377,3403],[10,2599],{"className":2600,"date":2601},[13],"2026-02-19",[16,2603,2595],{"id":2604},"trypema-rate-limiter",[20,2606,2607],{},"Trypema is a Rust sliding-window rate limiter with local, Redis, and hybrid providers. Each\nprovider supports binary admission through the Absolute strategy and gradual probabilistic load\nshedding through the Suppressed strategy.",[28,2609],{"className":2610},[76],[20,2612,2613],{},"Version 2.0.0 is released. It is a breaking redesign: providers are\nconstructed independently, configuration uses validated semantic types, reads expose live state,\nand conditional writes return structured outcomes.",[28,2615],{"className":2616},[76],[24,2618],{":tags":2619},"[\"Rust\", \"Rate Limiting\", \"Redis\", \"Library\", \"Distributed Systems\"]",[28,2621,2623,2632,2641,2651],{"className":2622},[31,32,33,34,35,36],[38,2624,2627,2630],{"className":2625,"href":2404,"rel":2626,"target":49},[44,45,46,47,48],[42],[20,2628,2629],{},"Open Docs",[54,2631],{"icon":56},[38,2633,2637,2639],{"className":2634,"href":2635,"rel":2636,"target":49},[44,45,46,47,48],"https://github.com/dev-davexoyinbo/trypema",[42],[20,2638,1650],{},[54,2640],{"icon":56},[38,2642,2646,2649],{"className":2643,"href":2644,"rel":2645,"target":49},[44,45,46,47,704,705,706],"https://docs.rs/trypema",[42],[20,2647,2648],{},"API Docs",[54,2650],{"icon":56},[38,2652,2655],{"className":2653,"href":2654},[44,45,46,47,704,705,706],"/engineering-logs/trypema-origin-story",[20,2656,2657],{},"Origin Story",[58,2659],{"alt":2660,"className":2661,"image":2417},"Local, Redis, and hybrid Trypema provider topologies",[62,63],[66,2663,2665],{"id":2664},"provider-choice","Provider choice",[185,2667,2668,2681],{},[188,2669,2670],{},[191,2671,2672,2675,2678],{},[194,2673,2674],{},"Provider",[194,2676,2677],{},"State and I/O",[194,2679,2680],{},"Best fit",[208,2682,2683,2694,2704],{},[191,2684,2685,2688,2691],{},[213,2686,2687],{},"Local",[213,2689,2690],{},"In-process, synchronous",[213,2692,2693],{},"One process owns the limit",[191,2695,2696,2698,2701],{},[213,2697,2382],{},[213,2699,2700],{},"Shared Redis state, async I/O per operation",[213,2702,2703],{},"Instances need the freshest shared view",[191,2705,2706,2709,2712],{},[213,2707,2708],{},"Hybrid",[213,2710,2711],{},"Local admission with periodic Redis synchronization",[213,2713,2714],{},"High throughput with bounded visibility lag",[28,2716],{"className":2717},[2718],"mb-4",[20,2720,2721,2722,2725,2726,2729],{},"Redis and hybrid require Redis 7.2+ and exactly one runtime feature: ",[177,2723,2724],{},"redis-tokio"," or\n",[177,2727,2728],{},"redis-smol",". Admission is best-effort under concurrency; concurrent callers can temporarily\novershoot a limit.",[28,2731],{"className":2732},[723],[66,2734,2736],{"id":2735},"two-pressure-strategies","Two pressure strategies",[20,2738,2739,2742,2743,2746,2747,2750,2751,2754],{},[1772,2740,2741],{},"Absolute"," returns an allowed or rejected decision. Rejections include ",[177,2744,2745],{},"retry_after: Duration",",\nthe configured ",[177,2748,2749],{},"window_size",", and ",[177,2752,2753],{},"remaining_after_waiting",", the capacity released when the oldest\nlive bucket expires.",[28,2756],{"className":2757},[2718],[20,2759,2760,2763,2764,2767],{},[1772,2761,2762],{},"Suppressed"," progressively increases the probability of shedding work as pressure rises, then\nuses the hard limit as its final boundary. Reads return a ",[177,2765,2766],{},"SuppressedRateLimitSnapshot"," containing\ntotal usage, declined usage, and the current suppression factor.",[336,2769],{"alt":2770,"className":2771,"image":2772},"Traffic pressure splitting into admitted and shed work",[340],"/projects/trypema-rate-limiter/probabilistic-suppression.webp",[66,2774,2776],{"id":2775},"live-state-and-conditional-updates","Live state and conditional updates",[20,2778,2779,2780,2782],{},"Absolute ",[177,2781,1322],{}," calls return the live total; suppressed reads return a structured snapshot. Unknown\nkeys produce zero-valued results without creating state. Expired history may be evicted lazily\nduring a read.",[28,2784],{"className":2785},[2718],[20,2787,2788,2790,2791,2794,2795,2798,2799,2802],{},[177,2789,1897],{}," stores a key's computed window capacity when the key is created. Later increments keep that\ncapacity until a matched conditional write replaces it. ",[177,2792,2793],{},"set_if"," replaces history after a\ncomparator match; ",[177,2796,2797],{},"set_if_preserve_history"," can retain the newest or oldest side. Both return\n",[177,2800,2801],{},"ConditionalSetOutcome",", and a matched zero removes the key.",[336,2804],{"alt":2805,"className":2806,"image":2807},"Conditional comparison leading to replacement or history preservation",[340],"/projects/trypema-rate-limiter/conditional-updates.webp",[66,2809,2811],{"id":2810},"v2-local-quickstart","v2 local quickstart",[764,2813,2815],{"className":1191,"code":2814,"language":1193,"meta":628,"style":628},"[dependencies]\ntrypema = \"2\"\n",[177,2816,2817,2825],{"__ignoreMap":628},[772,2818,2819,2821,2823],{"class":774,"line":775},[772,2820,930],{"class":791},[772,2822,1202],{"class":782},[772,2824,1205],{"class":791},[772,2826,2827,2830],{"class":774,"line":629},[772,2828,2829],{"class":791},"trypema = ",[772,2831,2832],{"class":1136},"\"2\"\n",[28,2834],{"className":2835},[2718],[764,2837,2839],{"className":766,"code":2838,"language":768,"meta":628,"style":628},"use trypema::{\n    BucketSize, RateLimit, RateLimitDecision, RateLimiterBuilder, WindowSize,\n    local::LocalRateLimiterProvider,\n};\n\nlet provider = LocalRateLimiterProvider::builder()\n    .window_size(WindowSize::minutes_or_panic(1))\n    .bucket_size(BucketSize::milliseconds_or_panic(10))\n    .build()\n    .unwrap();\n\nlet rate = RateLimit::per_second_or_panic(5.0);\n\nmatch provider.absolute().inc(\"user_123\", &rate, 1) {\n    RateLimitDecision::Allowed => {}\n    RateLimitDecision::Rejected { retry_after, .. } => {\n        println!(\"retry in {retry_after:?}\");\n    }\n    RateLimitDecision::Suppressed { .. } => unreachable!(),\n}\n",[177,2840,2841,2853,2880,2892,2896,2900,2919,2941,2965,2974,2984,2988,3013,3017,3052,3069,3093,3106,3112,3135],{"__ignoreMap":628},[772,2842,2843,2845,2848,2850],{"class":774,"line":775},[772,2844,779],{"class":778},[772,2846,2847],{"class":782}," trypema",[772,2849,786],{"class":778},[772,2851,2852],{"class":791},"{\n",[772,2854,2855,2858,2860,2863,2865,2868,2870,2873,2875,2878],{"class":774,"line":629},[772,2856,2857],{"class":782},"    BucketSize",[772,2859,1043],{"class":791},[772,2861,2862],{"class":782},"RateLimit",[772,2864,1043],{"class":791},[772,2866,2867],{"class":782},"RateLimitDecision",[772,2869,1043],{"class":791},[772,2871,2872],{"class":782},"RateLimiterBuilder",[772,2874,1043],{"class":791},[772,2876,2877],{"class":782},"WindowSize",[772,2879,872],{"class":791},[772,2881,2882,2885,2887,2890],{"class":774,"line":800},[772,2883,2884],{"class":782},"    local",[772,2886,786],{"class":778},[772,2888,2889],{"class":782},"LocalRateLimiterProvider",[772,2891,872],{"class":791},[772,2893,2894],{"class":774,"line":812},[772,2895,1049],{"class":791},[772,2897,2898],{"class":774,"line":824},[772,2899,797],{"emptyLinePlaceholder":647},[772,2901,2902,2904,2907,2909,2912,2914,2917],{"class":774,"line":845},[772,2903,1058],{"class":778},[772,2905,2906],{"class":791}," provider ",[772,2908,1064],{"class":778},[772,2910,2911],{"class":782}," LocalRateLimiterProvider",[772,2913,786],{"class":778},[772,2915,2916],{"class":782},"builder",[772,2918,1074],{"class":791},[772,2920,2921,2923,2925,2927,2929,2931,2934,2936,2938],{"class":774,"line":861},[772,2922,1079],{"class":778},[772,2924,2749],{"class":782},[772,2926,911],{"class":791},[772,2928,2877],{"class":782},[772,2930,786],{"class":778},[772,2932,2933],{"class":782},"minutes_or_panic",[772,2935,911],{"class":791},[772,2937,1907],{"class":917},[772,2939,2940],{"class":791},"))\n",[772,2942,2943,2945,2948,2950,2953,2955,2958,2960,2963],{"class":774,"line":875},[772,2944,1079],{"class":778},[772,2946,2947],{"class":782},"bucket_size",[772,2949,911],{"class":791},[772,2951,2952],{"class":782},"BucketSize",[772,2954,786],{"class":778},[772,2956,2957],{"class":782},"milliseconds_or_panic",[772,2959,911],{"class":791},[772,2961,2962],{"class":917},"10",[772,2964,2940],{"class":791},[772,2966,2967,2969,2972],{"class":774,"line":881},[772,2968,1079],{"class":778},[772,2970,2971],{"class":782},"build",[772,2973,1074],{"class":791},[772,2975,2976,2978,2981],{"class":774,"line":886},[772,2977,1079],{"class":778},[772,2979,2980],{"class":782},"unwrap",[772,2982,2983],{"class":791},"();\n",[772,2985,2986],{"class":774,"line":902},[772,2987,797],{"emptyLinePlaceholder":647},[772,2989,2990,2992,2995,2997,3000,3002,3005,3007,3010],{"class":774,"line":948},[772,2991,1058],{"class":778},[772,2993,2994],{"class":791}," rate ",[772,2996,1064],{"class":778},[772,2998,2999],{"class":782}," RateLimit",[772,3001,786],{"class":778},[772,3003,3004],{"class":782},"per_second_or_panic",[772,3006,911],{"class":791},[772,3008,3009],{"class":917},"5.0",[772,3011,3012],{"class":791},");\n",[772,3014,3015],{"class":774,"line":983},[772,3016,797],{"emptyLinePlaceholder":647},[772,3018,3019,3022,3025,3027,3029,3031,3033,3035,3037,3040,3042,3044,3047,3049],{"class":774,"line":1013},[772,3020,3021],{"class":778},"match",[772,3023,3024],{"class":791}," provider",[772,3026,942],{"class":778},[772,3028,2390],{"class":782},[772,3030,1325],{"class":791},[772,3032,942],{"class":778},[772,3034,1897],{"class":782},[772,3036,911],{"class":791},[772,3038,3039],{"class":1136},"\"user_123\"",[772,3041,1043],{"class":791},[772,3043,914],{"class":778},[772,3045,3046],{"class":791},"rate, ",[772,3048,1907],{"class":917},[772,3050,3051],{"class":791},") {\n",[772,3053,3055,3058,3060,3063,3066],{"class":774,"line":3054},15,[772,3056,3057],{"class":782},"    RateLimitDecision",[772,3059,786],{"class":778},[772,3061,3062],{"class":782},"Allowed",[772,3064,3065],{"class":778}," =>",[772,3067,3068],{"class":791}," {}\n",[772,3070,3072,3074,3076,3079,3082,3085,3088,3091],{"class":774,"line":3071},16,[772,3073,3057],{"class":782},[772,3075,786],{"class":778},[772,3077,3078],{"class":782},"Rejected",[772,3080,3081],{"class":791}," { retry_after, ",[772,3083,3084],{"class":778},"..",[772,3086,3087],{"class":791}," } ",[772,3089,3090],{"class":778},"=>",[772,3092,821],{"class":791},[772,3094,3096,3099,3101,3104],{"class":774,"line":3095},17,[772,3097,3098],{"class":782},"        println!",[772,3100,911],{"class":791},[772,3102,3103],{"class":1136},"\"retry in {retry_after:?}\"",[772,3105,3012],{"class":791},[772,3107,3109],{"class":774,"line":3108},18,[772,3110,3111],{"class":791},"    }\n",[772,3113,3115,3117,3119,3121,3123,3125,3127,3129,3132],{"class":774,"line":3114},19,[772,3116,3057],{"class":782},[772,3118,786],{"class":778},[772,3120,2762],{"class":782},[772,3122,1003],{"class":791},[772,3124,3084],{"class":778},[772,3126,3087],{"class":791},[772,3128,3090],{"class":778},[772,3130,3131],{"class":782}," unreachable!",[772,3133,3134],{"class":791},"(),\n",[772,3136,3138],{"class":774,"line":3137},20,[772,3139,878],{"class":791},[28,3141],{"className":3142},[2718],[20,3144,3145,3146,3149,3150,3153,3154,2184,3157,3160],{},"Builders return an ",[177,3147,3148],{},"Arc"," and start stale-state cleanup by default. Use ",[177,3151,3152],{},"disable_cleanup()"," while\nbuilding when cleanup is unwanted; ",[177,3155,3156],{},"start_cleanup_loop()",[177,3158,3159],{},"stop_cleanup_loop()"," are\nidempotent after construction.",[28,3162],{"className":3163},[723],[66,3165,3167],{"id":3166},"redis-and-hybrid-construction","Redis and hybrid construction",[764,3169,3171],{"className":1191,"code":3170,"language":1193,"meta":628,"style":628},"[dependencies]\ntrypema = { version = \"2\", features = [\"redis-tokio\"] }\nredis = { version = \"1\", features = [\"aio\", \"tokio-comp\", \"connection-manager\"] }\ntokio = { version = \"1\", features = [\"full\"] }\n",[177,3172,3173,3181,3196,3221],{"__ignoreMap":628},[772,3174,3175,3177,3179],{"class":774,"line":775},[772,3176,930],{"class":791},[772,3178,1202],{"class":782},[772,3180,1205],{"class":791},[772,3182,3183,3186,3189,3191,3194],{"class":774,"line":629},[772,3184,3185],{"class":791},"trypema = { version = ",[772,3187,3188],{"class":1136},"\"2\"",[772,3190,1216],{"class":791},[772,3192,3193],{"class":1136},"\"redis-tokio\"",[772,3195,1222],{"class":791},[772,3197,3198,3201,3204,3206,3209,3211,3214,3216,3219],{"class":774,"line":800},[772,3199,3200],{"class":791},"redis = { version = ",[772,3202,3203],{"class":1136},"\"1\"",[772,3205,1216],{"class":791},[772,3207,3208],{"class":1136},"\"aio\"",[772,3210,1043],{"class":791},[772,3212,3213],{"class":1136},"\"tokio-comp\"",[772,3215,1043],{"class":791},[772,3217,3218],{"class":1136},"\"connection-manager\"",[772,3220,1222],{"class":791},[772,3222,3223,3226,3228,3230,3233],{"class":774,"line":812},[772,3224,3225],{"class":791},"tokio = { version = ",[772,3227,3203],{"class":1136},[772,3229,1216],{"class":791},[772,3231,3232],{"class":1136},"\"full\"",[772,3234,1222],{"class":791},[28,3236],{"className":3237},[2718],[20,3239,3240,3241,3244,3245,3247,3248,3251],{},"Redis and hybrid providers receive a Redis connection directly through their own builders. Hybrid\nalso accepts a validated ",[177,3242,3243],{},"SyncInterval","; smaller values reduce visibility lag and increase Redis\nwork. Its authoritative ",[177,3246,1322],{}," synchronizes with Redis and overlays local pending counts, while\n",[177,3249,3250],{},"get_estimate"," may answer from initialized local state.",[336,3253],{"alt":3254,"className":3255,"image":3256},"Migration from the v1 facade to independently built v2 providers",[340],"/projects/trypema-rate-limiter/v2-migration.webp",[66,3258,3260],{"id":3259},"benchmark-snapshot","Benchmark snapshot",[20,3262,3263],{},"The published benchmarks remain historical measurements, not universal performance promises.\nThey compare fixed workloads across throughput, tail latency, and hot-key contention. Results vary\nwith hardware, runtime, Redis placement, configuration, and competing load. The Redis and hybrid\nresults compare backend cost, not identical behaviour, because the hybrid fast path accepts bounded\nvisibility lag between synchronizations.",[28,3265],{"className":3266},[2718],[1488,3268,3269,3272,3275],{},[1491,3270,3271],{},"Local 100,000-key workload: Trypema Absolute reached 9.64M ops/s and Suppressed reached 7.61M ops/s, compared with Governor at 6.28M ops/s.",[1491,3273,3274],{},"Redis hot-key workload: Trypema Absolute reached 47.6k ops/s and Suppressed reached 41.0k ops/s, compared with redis-cell at 64.4k ops/s.",[1491,3276,3277],{},"Hybrid hot-key fast path with a 10 ms synchronization interval: Trypema Absolute reached 11.36M ops/s and Suppressed reached 10.57M ops/s, while direct Redis alternatives remained in the tens of thousands of operations per second.",[336,3279],{"alt":3280,"className":3281,"image":3282},"Benchmark instruments for throughput, tail latency, and contention",[340],"/projects/trypema-rate-limiter/benchmark-measurements.webp",[28,3284,3286,3296],{"className":3285},[31,32,33,34,1460],[38,3287,3291,3294],{"className":3288,"href":3289,"rel":3290,"target":49},[44,45,46,47,704,705,706],"https://trypema.davidoyinbo.com/benchmarks/benchmark-results/local-benchmark-comparison",[42],[20,3292,3293],{},"Local Results",[54,3295],{"icon":56},[38,3297,3301,3304],{"className":3298,"href":3299,"rel":3300,"target":49},[44,45,46,47,704,705,706],"https://trypema.davidoyinbo.com/benchmarks/benchmark-results/redis-benchmark-comparison",[42],[20,3302,3303],{},"Redis Results",[54,3305],{"icon":56},[66,3307,3309],{"id":3308},"v2-highlights","v2 highlights",[28,3311,3313,3338,3351,3364],{"className":3312},[1468,1469,1470,1471,1472],[1474,3314,3316,3320],{"className":3315},[1477],[759,3317,3319],{"id":3318},"validated-configuration","Validated configuration",[28,3321,3323],{"className":3322},[1485,1486],[20,3324,3325,1043,3327,1043,3329,1043,3332,2750,3335,3337],{},[177,3326,2877],{},[177,3328,2952],{},[177,3330,3331],{},"HardLimitFactor",[177,3333,3334],{},"SuppressionFactorCachePeriod",[177,3336,3243],{},"\nmake units and constraints explicit.",[1474,3339,3341,3345],{"className":3340},[1477],[759,3342,3344],{"id":3343},"live-reads","Live reads",[28,3346,3348],{"className":3347},[1485,1486],[20,3349,3350],{},"Absolute totals and suppressed snapshots include only live buckets; unknown keys remain absent.",[1474,3352,3354,3358],{"className":3353},[1477],[759,3355,3357],{"id":3356},"safe-reconciliation","Safe reconciliation",[28,3359,3361],{"className":3360},[1485,1486],[20,3362,3363],{},"Comparator-gated writes replace history or preserve its newest or oldest side, with zero deleting\nstate.",[1474,3365,3367,3371],{"className":3366},[1477],[759,3368,3370],{"id":3369},"cleanup-controls","Cleanup controls",[28,3372,3374],{"className":3373},[1485,1486],[20,3375,3376],{},"Cleanup starts by default and can be disabled or controlled through idempotent start and stop\nmethods.",[28,3378,3381,3390,3398],{"className":3379},[31,32,33,34,3380],"mt-10",[38,3382,3385,3388],{"className":3383,"href":2404,"rel":3384,"target":49},[44,45,46,47,48],[42],[20,3386,3387],{},"Read the Docs",[54,3389],{"icon":56},[38,3391,3394,3396],{"className":3392,"href":2635,"rel":3393,"target":49},[44,45,46,47,48],[42],[20,3395,698],{},[54,3397],{"icon":56},[38,3399,3401],{"className":3400,"href":1590},[44,45,46,47,704,705,706],[20,3402,1593],{},[1595,3404,3405],{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}",{"title":628,"searchDepth":629,"depth":629,"links":3407},[3408,3409,3410,3411,3412,3413,3414],{"id":2664,"depth":629,"text":2665},{"id":2735,"depth":629,"text":2736},{"id":2775,"depth":629,"text":2776},{"id":2810,"depth":629,"text":2811},{"id":3166,"depth":629,"text":3167},{"id":3259,"depth":629,"text":3260},{"id":3308,"depth":629,"text":3309},"2026-02-19T00:00:00.000Z","High-performance sliding-window rate limiting for Rust with independently built local, Redis, and hybrid providers.",{"license":1611},{"title":2595,"description":3416},"projects/trypema-rate-limiter",[652,3421,2382,1617,654],"Rate Limiting","4NLdp9roh9lmIfuMuJ8EjdcbsOAG39L6MiCinODalYQ",1786978702088]