Technical Writeup
ScurryPy is a discord API wrapper written in Python that aims to provide clarity over magic. While this may seem straightforward at first glance, especially when other libraries have already implemented it, there are plenty of technical accomplishments that help ScurryPy stand out from other trending libraries; most notably, the core is under 1000 lines of code.
Introduction¶
So what makes ScurryPy so effective? Caching? No. ScurryPy does not assume what the user needs nor does it try to guess what the user wants to do. Avoiding circular imports with annotations? Nope. All classes have clearly set boundaries for what they do so there is no need for annotations. As a result, ScurryPy is modular in that it provides you with the building blocks, and you do what you want with them (guided by Discord’s API). These are just the highlights of what clarity over magic can bring to the table.
This writeup will cover the main parts of ScurryPy including the HTTP and rate limiting, Gateway logic and sharding, and how the Client glues this all together without being a god object. Don’t let the terms intimidate you. Each section will have a mental map to help visualize steps. By the end of this writeup, you'll understand how clarity over magic leads to better software - and how to implement Discord's most notoriously difficult features.
HTTP Client¶
Before digging in with the HTTP Client, this section, and future sections, will lead with a mental map to help guide the explanation.

Figure 1: HTTP Client Mental Map
The HTTP Client is responsible for queuing and sending requests. That’s it. No fancy abstractions. No layers of indirection. Just. Sending and documenting the requests. This sets up independence such that when adding an endpoint, fire a request and think of nothing else. If it’s a GET request, expect data to return. ScurryPy marks all GET endpoints with fetch_{object}. The client takes the parameters you’d expect to need to make a request, like endpoint and method, and spits back out the JSON object from Discord.
Now, let’s go through the map step by step:
- Send request via GET to endpoint, say, “channels/123/messages”
- The request is put into a queue based on this endpoint with a lock.
- A lock coordinates updates to the dictionary that maps endpoints to their queues.
- Each queue has a dedicated worker to handle the request.
- The worker sends the HTTP request and waits for Discord's response.
- Using the bucket ID from Discord's response, update or create the bucket's rate limit info (with proper lock coordination).
- A lock coordinates lock access itself and then a lock per bucket ID.
- If the rate limit is exhausted (no remaining requests), then create a sleep task. This is proactive rate limiting so no 429 is returned.
- If another request targets the same bucket, it waits for the sleep task to finish.
Done.
Key Idea: Each endpoint gets its own queue and worker, and each worker enforces proactive rate limiting using Discord’s bucket replies.
Gateway Client¶
Gateway is more straightforward. Let’s start with the mental map.

Figure 2: Gateway Client Mental Map
The Gateway Client is self-sufficient in that it handles its own connection (or shard) to the Gateway. That is all this client is for. The beauty of this design is that sharding is the matter of spawning the needed number of shards at the rate of the max concurrency as recommended by Discord. What if you need to access a specific shard or know what event a shard came from? Not a problem! Use Discord’s formula: (guild_id >> 22) % shard_count.
Let’s dig into the steps of the mental map:
- Connect to the gateway with the URL provided by the GET /gateway/bot endpoint and use the URL params “?v=10&encoding=json”. URL params set the stage for how Discord should send its data packets. In this case, the URL signals using JSON encoding: Discord serializes events as JSON strings to be deserialized into Python dictionaries.
- Await the HELLO event Discord sends back when you connect.
- Use this to set heartbeat intervals and initialize the sequence number.
- This is when the heartbeat task is started to keep the session alive. Fire-and-forget.
- Send IDENTIFY through the websocket.
- That’s it for new shards or connections. At this point, it’s about listening to certain OP codes. Only OP codes that impact the connection state are listed.
- OP 0: dispatch. Enqueue this for the Client to pick up.
- OP 7: a disconnect occurred and Discord wants you to RESUME.
- OP 9: a disconnect occurred causing the session to be invalid. Discord wants you to start fresh.
Done. This is the minimal set of logic required to maintain a stable gateway connection.
Key Idea: Each connection or shard gets its own task to connect, identify, reconnect or re-identify if needed, listen to events, and eventually close.
Bot Client¶
Almost there, if you’ve read this far, you’re doing great! Know that from here on out, it’s dead simple. The mental map for the Bot Client looks something like:

Figure 3: Bot Client Mental Map
The Bot Client is intentionally not a god object. It is really just an orchestrator - it uses the HTTP and Gateway clients you've already seen. There are parts that were left out of the map such as the decorators to register commands and resource fetching helpers so the user doesn’t have to touch the HTTP Client directly. But let’s be honest, connection logic is the meat of the Bot Client. And that’s all. Let’s go through the mental map step by step:
- Start the HTTP Client. No strings attached.
- Run setup hooks. This is an array of callable functions that the user can set using the “setup_hook” decorators. These hooks run once for the entire life cycle of the bot. Need to start a database? This is where you do it!
- Sync slash commands (guild and global). This is just toggled with a setting in the Bot Client’s constructor for convenience.
- Fetch the gateway info. This is where important start up info is located such as the total number of recommended shards, how many shards can be started concurrently, and the recommended gateway connect URL.
- Then based on the gateway info, as shards start up, fire a task to listen to each shard.This is a task per shard and each shard has its own event queue so the listener doesn’t need to know anything about what shard it is, just direct the event accordingly. Listening and starting occur concurrently. So as shards are starting up, the shards already started can start taking in events as soon as they’re ready. The “sleep 5 seconds” is waiting per batch to not spam Discord’s gateway.
Done.
Key Idea: The Client's sole purpose is to provide a thin abstraction layer between internals and the user-facing API.
Data Model¶
The final piece: how to turn Discord's JSON into clean, type-safe Python objects. DataModel is a class that transforms Discord's JSON into usable Python objects and back again when needed. A user could just access the dictionary itself, sure. But, what if dictionaries could be represented as a class so instead of event[‘data’], it looks like event.data? This is where the DataModel class comes in. Let’s take a look at the mental map

Figure 4: Data Model Mental Map
This one is straightforward. Let’s dig into the step by step:
- The
from_dictfunction hydrates a dataclass by iterating over its fields and pulling matching values from Discord's JSON payload. If a field isn't in the payload, it's skipped; no guessing. The result? Clean dot notation (event.data) instead of dictionary access (event['data']). - When you need to send data back to Discord,
to_dictdoes the reverse: converts the dataclass attributes back into a dictionary.
Done.
You might be wondering: what about nested objects like a Message containing a User? DataModel handles that recursively. It calls from_dict or to_dict on child DataModels automatically.
Key Idea: This DataModel class exists to deserialize Discord's payloads and serialize dataclasses for fluent communication between ScurryPy and Discord.
Conclusion¶
Because every subsystem is small, explicit, and self-contained, extending ScurryPy is almost trivial: add a model, add a resource, and use the HTTP client directly. You can prototype new Discord features the day they release, build custom caching, or omit caching entirely. The library never fights you; it never assumes what you want. That’s the benefit of clarity over magic.