This is a collaborative piece written by Jared Miller and Ayu.
Despite virtually every online platform facing these sorts of challenges, many are left to reinvent tools from scratch, with varying degrees of success. We’d like to help our fellow companies get a head-start on their safety measures — that's why, in partnership with ROOST and the internet.dev team, we’re excited to open-source Osprey: our safety rules engine.
With Osprey, teams can investigate real-time activities across their platforms and quickly deploy dynamic rules to address emerging threats, all with minimal engineering overhead.
This post will walk you through what Osprey is, how it works, and how your team can start using it to build stronger safety measures.
Fighting bad actors requires tools that can adapt to new challenges in real-time. A service as large as Discord needs a system that can:
Process at Scale: Handle thousands of events per second, in real-time, and scale seamlessly as our platform grows.
Enable Rapid Response: Let teams write expressive rules that take effect in minutes.
Provide Clear Decisions: Deliver actionable verdicts on whether ongoing user activities are safe, suspicious, or malicious.
Show Its Work: Offer transparency into how rules were executed and when errors arise, making investigating and debugging straightforward.
Learn and Adapt: Support a continuous feedback loop where detection insights improve future rules.
Stay Future-Proof: Accommodate extensibility for new features to combat attack patterns we have yet to imagine.
These requirements shaped every architectural decision we made when developing Osprey, from its rule language to its distributed processing model.
How does it work?
Key Concepts
Osprey is built around several core concepts. It ingests Actions, either synchronously via GRPC or asynchronously via a Message Queue, and runs them through a series of Rules written in SML (just Some Made Up Language) that can be expanded upon with UDFs (User Defined Functions), and processes them in a combination of Features, Entities, and Effects, some of which may apply to Entities. Finally, some synchronous actions in particular also return Verdicts, which inform the caller about any determinations made by the rules. All outputs are sent to our Apache Druid cluster, which backs our investigations UI.
Easy right!
… okay, that was a LOT of bolded words and new terms thrown around. Let’s drill down what all these terms mean:
Actions
Actions are events that we send to Osprey. Each type of action has a unique ID and schema. They’re effectively JSON blobs consumed by the rules engine, and can be customized to contain whatever data the caller provides.
Rules are the heart of Osprey. Within the main Osprey “engine,” we define a rules language that we call SML (Some Made-up Language). Writing rules should be simple and accessible to folks with minimal technical knowledge, so we based it on Python! Even with a fairly simple syntax, we can make powerful statements, with rules capable of referencing other rules and data.
The rules language enables static validation that can enforce a particular way rules should be written. Provided that validation is consistent, new validation logic can be easily added via Python code. It can be as simple as requiring all variable names to start with an uppercase letter, or as complicated as a given case needs.
UDFs are functions written in (actual, not SML) Python that can be called anywhere within the rules. UDFs define the standard library for Osprey, such as Rule (here), WhenRules (here), and JsonData (here), to name a few. UDFs are how you’ll be expanding upon Osprey’s language functionality if you decide to incorporate Osprey into your own products.
To give an example, if rule writers want to access data from an external service, one may define a UDF to call said service and present the result.
@registerclassLinkSpamScore(HasHelper[LinkSpamScoreProvider], UDFBase[LinkSpamArguments, float]):"""
Get a float [0,1] score from link spam model
""" category: ClassVar[str] = UdfCategories.ML
execute_async: ClassVar[bool] = Truedefexecute(self, execution_context: ExecutionContext, arguments: LinkSpamArguments) -> float: provider = execution_context.get_udf_helper(self)
accessor = execution_context.get_external_service_accessor(provider)
response: PredictResponse = accessor.get(arguments)
return response.score
Features
A feature is any variable in the global namespace in Osprey. All features must be uniquely named. However, prefixing a `_` at the start of a variable name prevents it from being exported as a feature and keeps the variable within the local file’s namespace.
Features are outputs of Osprey executions. Downstream, they are sent to and indexed by Druid, so users can query for events based on feature names later, i.e. `UserEmail == '[email protected]`.
In the example above, both UserId and UserEmail are features.
Entities
Entities are a special type of Feature. All entities are features, but not all features are entities. Within Discord, these represent persistent units like Users, Servers, or emails.
An entity can have effects applied to it, such as labels, classifications, or signals. Every entity has a type that determines which effects can be applied to it based on static validations.
Entities get special treatment within the Osprey UI. Clicking on an entity in the tool will take you to an Entity View, providing a deep dive into its history.
Effects
Effects can be triggered when one or more rules are evaluated to be true. These are validated and handled in aggregate at the end of an execution output. For example, an effect might apply a label to an entity, marking it as a “Spammer”.
The actual Osprey system is built from a few independently operating services. Actions are first sent to the Osprey Coordinator, which acts as an intermediary between a fleet of Osprey Rules Workers, balancing asynchronous and synchronous requests between them. Rules workers have a locally mounted copy of the Rules.
At Discord, we use ETCD to distribute rules to the workers. This lets us push new rules into production without requiring deployment. When the rules worker finishes evaluating an Action, it outputs the result to a configurable set of Output Sinks. These take the results and apply or write them as needed. Among the Output Sinks, one provided by default is a publisher to a Kafka queue, which pipes execution results into a Druid database. All these power investigations via our Osprey UI.
Investigative Tooling: The Osprey UI
The Osprey UI is our real-time investigation platform for tracking bad actors, analyzing rule performance, and reviewing execution results. Using our custom query language, security teams can run complex queries against our Druid database to spot trends and identify new attack patterns.
The interface (displayed below with dummy data) combines an event stream with visualization tools, such as time series charts and Top N tables. This allows teams to spot anomalies quickly, whether it's unusual spikes in account creation, patterns in guild joining behavior, or coordinated Direct Message campaigns. The feedback loop is immediate: insights from investigations directly inform new rules and protections.
Integrated Investigation Workflow
The Osprey UI treats Entities (like User IDs, Guild IDs, IP addresses), Features (contextual data like usernames), and Effects (like applied labels) as first-class components. This creates a natural investigation workflow where, when examining a suspicious entity, investigators can instantly see all related events, applied labels, and behavioral patterns in one unified view. The interface below uses dummy data to show a User Entity view:
Handling Sensitive Data
Osprey is hosted on your own infrastructure and can only access data and information you send to it. At Discord, since we use Osprey to respond to incidents that can contain sensitive user data, we maintain strict access controls and audit trails. Team members can only access certain actions or entity-specific views with proper justification and permissions, ensuring user privacy while enabling effective investigations.
Operating at Scale
Osprey was built to scale. As of December 2025, Discord uses the open source version of Osprey to handle ~400 million actions per day. Although mostly benign, these events provide valuable signals that are then categorized across 204 action types and 2288 rules, with the average action triggering ~500 rules. Among the rules, we’ve included 99 custom UDFs, many of which perform RPC to other internal services. Individual performance mileage varies based on the amount and complexity of the rules being triggered.
The system can support more actions by horizontally scaling the number of rule worker instances. Running the Osprey Coordinator helps balance synchronous and asynchronous actions across the rule workers. This adds some resilience against spiky traffic patterns without over-provisioning the number of rules workers and minimizing the need to add workers on demand.
Optimizing Osprey's performance remains a high priority for Discord. By reducing costs and increasing rule executions per second, we can directly enhance Osprey's impact and effectiveness as a safety tool.
Customizing Osprey for the Public
There is no one-size-fits-all rules engine that can efficiently meet all the needs of every adopter. In its original form, Osprey was built using Discord’s internal libraries, conforming to our internal infrastructure and use cases.
From the beginning of our open-sourcing journey, we knew configurability would be essential. We started planning by looking at our complex and featureful system, stripping away components bit by bit until we arrived at a minimal, yet fully-featured, product.
To achieve this, we needed to make some changes to our system:
The Osprey Rule engine should accept events sent via GRPC requests or via a Message Queue (PubSub or Kafka) as opposed to only accepting events via Osprey Coordinator.
We should support open-source alternatives to any proprietary dependencies being used. For example, where we used PubSub, we also want to support Apache Kafka, which can be self-hosted.
Only a small set of Discord’s UDFs, particularly simple ones without external dependencies, would be included out of the box. We should provide an adaptor layer for users to easily incorporate their own custom UDFs.
We should support loading Rules via the file system at build time. The ability to hotload rules via ETCD would be maintained, but we didn’t want to require an extra dependency and configuration step for those who could do without.
The rules engine should return Verdicts. These could be ignored if passing in actions asynchronously, but they serve as a clear communication method to synchronous callers.
We should support arbitrary Output Sinks so users can process execution results as they wish.
We also wanted to provide optional extra features to everyone:
A UI investigation tool. This required having an Output Sink publishing to a Kafka topic and subscribed to via Druid.
The Osprey Coordinator which can act as a load-balancer to prioritize synchronous actions when facing surges of asynchronous actions. It’s particularly useful in environments where traffic can be bursty. For systems that don’t require the Coordinator, such as ones with little traffic or upstream rate limiting, Actions can be sent directly to the rules workers using the same methods (GRPCs for synchronous and a Message Queue for asynchronous).
What we knew we wouldn’t be able to provide:
Our full set of UDFs and Rules. This included things like our Counter service, which is heavily integrated with our internal Scylla databases.
Our full set of Output Sinks. For example, we write all of our rule execution results to BigQuery. While this might be useful to some, it’s not strictly necessary, and we didn’t want to bias towards any non-open-sourced software.
Taking our existing rules engine and making it conform to these requirements, while still operating for our needs, was akin to repairing a car engine while driving down the highway. We made UDFs, Rules, and Output Sinks configurable by using the “Pluggy” Python library. We then removed and replaced internal Discord dependencies one by one, placing our internal configurations behind our new Pluggy integrations. And even as changes were being made, we carefully tested and deployed into our own production environment (but not at the same time), maintaining parity with our intended end product while weeding out potential bugs that could pop up along the way.
In the end, it took months of work among five incredible engineers to carve out our tool for the public.
What’s Osprey Flying Towards Next?
Open-sourcing Osprey is just the beginning. We’ve got an ambitious roadmap ahead, focused on making the tool even more powerful and accessible for the community.
A sneak peek at some of our plans:
We’re looking into performance improvements to support even higher volumes of actions per second for the Rule Engine.
Upgrading dependencies to their latest versions to support newer, more advanced features.
Improving our documentation, including best practice guides and tutorials for using the service.
Build out a toolkit of open-sourced plugins for Osprey, like Counter and Label services.
Join Us in Building Better Safety Tools
The safety challenges facing online platforms are bigger than any one company can solve alone. That's why we are proud to participate in efforts like ROOST and why we're committed to building Osprey as a true community project. Whether you're contributing code, joining the public working group meetings, sharing use cases, or just providing feedback, your participation helps make the internet safer for everyone.
We actively encourage contributions of all kinds: new features, performance improvements, documentation, or creative applications we haven't thought of yet. Your unique perspective and challenges can help make Osprey better for the entire community.
At Discord, we believe the best safety innovations happen when we all work together. We're committed to long-term investment in Osprey's development and will continue working with our partners at ROOST to bring more powerful safety tools to the open source community.