diff --git a/crates/tor-netdoc/src/doc.rs b/crates/tor-netdoc/src/doc.rs
index bba117d916b173999da0ab57e80c4c4a44e924ce..b683f6c5363ddbbb4229fc1000089127c17b4223 100644
--- a/crates/tor-netdoc/src/doc.rs
+++ b/crates/tor-netdoc/src/doc.rs
@@ -29,6 +29,8 @@
 //! Finally, there are the voting documents themselves that authorities
 //! use in order to calculate the consensus.
 
+use crate::util::intern::InternCache;
+
 pub mod authcert;
 pub mod microdesc;
 pub mod netstatus;
@@ -42,3 +44,9 @@ pub mod routerdesc {
     /// The digest of a RouterDesc document, as reported in a NS consensus.
     pub type RdDigest = [u8; 20];
 }
+
+/// Cache of Protocols objects, for saving memory.
+//
+/// This only holds weak references to the objects, so we don't
+/// need to worry about running out of space because of stale entries.
+static PROTOVERS_CACHE: InternCache<tor_protover::Protocols> = InternCache::new();
diff --git a/crates/tor-netdoc/src/doc/netstatus/rs.rs b/crates/tor-netdoc/src/doc/netstatus/rs.rs
index 457198d7e090458ec28cf2644b697dedc78bb727..fde46fe3349e721b19ee5979757a26aa602eb77a 100644
--- a/crates/tor-netdoc/src/doc/netstatus/rs.rs
+++ b/crates/tor-netdoc/src/doc/netstatus/rs.rs
@@ -10,6 +10,7 @@ mod md;
 mod ns;
 
 use super::{NetstatusKwd, RelayFlags, RelayWeight};
+use crate::doc;
 use crate::parse::parser::Section;
 use crate::types::misc::*;
 use crate::types::version::TorVersion;
@@ -44,7 +45,7 @@ struct GenericRouterStatus<D> {
     /// Version of the software that this relay is running.
     version: Option<Version>,
     /// List of subprotocol versions supported by this relay.
-    protos: Protocols,
+    protos: Arc<Protocols>,
     /// Information about how to weight this relay when choosing a
     /// relay at random.
     weight: RelayWeight,
@@ -212,9 +213,11 @@ where
         // PR line
         let protos = {
             let tok = sec.required(RS_PR)?;
-            tok.args_as_str()
-                .parse::<Protocols>()
-                .map_err(|e| EK::BadArgument.at_pos(tok.pos()).with_source(e))?
+            doc::PROTOVERS_CACHE.intern(
+                tok.args_as_str()
+                    .parse::<Protocols>()
+                    .map_err(|e| EK::BadArgument.at_pos(tok.pos()).with_source(e))?,
+            )
         };
 
         // W line
diff --git a/crates/tor-netdoc/src/doc/netstatus/rs/build.rs b/crates/tor-netdoc/src/doc/netstatus/rs/build.rs
index d217eacf6b71e294017c8cc7e608c48dc9235c1a..6b6d843470646efb58ae6567e49036827b909bbe 100644
--- a/crates/tor-netdoc/src/doc/netstatus/rs/build.rs
+++ b/crates/tor-netdoc/src/doc/netstatus/rs/build.rs
@@ -1,6 +1,7 @@
 //! Provide builder functionality for routerstatuses.
 
 use super::{GenericRouterStatus, MdConsensusRouterStatus};
+use crate::doc;
 use crate::doc::microdesc::MdDigest;
 use crate::doc::netstatus::{ConsensusBuilder, RelayFlags, RelayWeight};
 use crate::{BuildError as Error, BuildResult as Result};
@@ -143,7 +144,7 @@ impl<D: Clone> RouterStatusBuilder<D> {
             addrs: self.addrs.clone(),
             doc_digest,
             version,
-            protos,
+            protos: doc::PROTOVERS_CACHE.intern(protos),
             flags: self.flags,
             weight,
         })
diff --git a/crates/tor-netdoc/src/doc/routerdesc.rs b/crates/tor-netdoc/src/doc/routerdesc.rs
index 4fc0f1817ad686b0cd8b91b0caf2c1b90ede4ce6..7986e62ab13844f46e2d5a6c1bc56678450622e5 100644
--- a/crates/tor-netdoc/src/doc/routerdesc.rs
+++ b/crates/tor-netdoc/src/doc/routerdesc.rs
@@ -39,7 +39,7 @@ use crate::types::family::RelayFamily;
 use crate::types::misc::*;
 use crate::types::policy::*;
 use crate::types::version::TorVersion;
-use crate::{AllowAnnotations, Error, ParseErrorKind as EK, Result};
+use crate::{doc, AllowAnnotations, Error, ParseErrorKind as EK, Result};
 
 use once_cell::sync::Lazy;
 use std::sync::Arc;
@@ -124,7 +124,7 @@ pub struct RouterDesc {
     /// (deprecated) TAP protocol.
     tap_onion_key: ll::pk::rsa::PublicKey,
     /// List of subprotocol versions supported by this relay.
-    proto: tor_protover::Protocols,
+    proto: Arc<tor_protover::Protocols>,
     /// True if this relay says it's a directory cache.
     is_dircache: bool,
     /// True if this relay says that it caches extrainfo documents.
@@ -543,10 +543,12 @@ impl RouterDesc {
         // List of subprotocol versions
         let proto = {
             let proto_tok = body.required(PROTO)?;
-            proto_tok
-                .args_as_str()
-                .parse::<tor_protover::Protocols>()
-                .map_err(|e| EK::BadArgument.at_pos(proto_tok.pos()).with_source(e))?
+            doc::PROTOVERS_CACHE.intern(
+                proto_tok
+                    .args_as_str()
+                    .parse::<tor_protover::Protocols>()
+                    .map_err(|e| EK::BadArgument.at_pos(proto_tok.pos()).with_source(e))?,
+            )
         };
 
         // tunneled-dir-server
diff --git a/crates/tor-protover/src/lib.rs b/crates/tor-protover/src/lib.rs
index 70896460a7512d0f72600fb5a159d2547326b31f..83e8ff787b1d9f7f29ee85b46d9892aff202a941 100644
--- a/crates/tor-protover/src/lib.rs
+++ b/crates/tor-protover/src/lib.rs
@@ -77,6 +77,7 @@ caret_int! {
     /// cbor document format in the walking onions proposal.
     ///
     /// For the full semantics of each subprotocol, see tor-spec.txt.
+    #[derive(Hash,Ord,PartialOrd)]
     pub struct ProtoKind(u16) {
         /// Initiating and receiving channels, and getting cells on them.
         Link = 0,
@@ -110,7 +111,7 @@ caret_int! {
 const N_RECOGNIZED: usize = 12;
 
 /// Representation for a known or unknown protocol.
-#[derive(Eq, PartialEq, Clone, Debug)]
+#[derive(Eq, PartialEq, Clone, Debug, Hash, Ord, PartialOrd)]
 enum Protocol {
     /// A known protocol; represented by one of ProtoKind.
     Proto(ProtoKind),
@@ -138,7 +139,7 @@ impl Protocol {
 /// Representation of a set of versions supported by a protocol.
 ///
 /// For now, we only use this type for unrecognized protocols.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
 struct SubprotocolEntry {
     /// Which protocol's versions does this describe?
     proto: Protocol,
@@ -157,7 +158,7 @@ struct SubprotocolEntry {
 /// use tor_protover::Protocols;
 /// let p: Result<Protocols,_> = "Link=1-3 LinkAuth=2-3 Relay=1-2".parse();
 /// ```
-#[derive(Debug, Clone, Default)]
+#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)]
 pub struct Protocols {
     /// A mapping from protocols' integer encodings to bit-vectors.
     recognized: [u64; N_RECOGNIZED],
@@ -398,6 +399,7 @@ impl std::str::FromStr for Protocols {
             let s: SubprotocolEntry = ent.parse()?;
             result.add(&mut foundmask, s)?;
         }
+        result.unrecognized.sort();
         Ok(result)
     }
 }
diff --git a/doc/semver_status.md b/doc/semver_status.md
index 5e5d7349469425cc682cdcaa3a40c7e96626dacb..c2e641131f5669a896565e7ed0a5a3f13a628836 100644
--- a/doc/semver_status.md
+++ b/doc/semver_status.md
@@ -45,14 +45,17 @@ arti-client:
   &Arc, not Arc.
 
 tor-dirmgr:
-
   new-api: DirMgrConfig object now has accessors.
 
+
 tor-netdoc:
 
   new-api (experimental only): Can modify the set of relays in an unverified
   consensus.
 
-
-tor-netdoc:
   api-break: changed the return type of GenericRouterStatus::version()
+
+tor-protover:
+  new-api: Protocols now implements Eq, PartialEq, and Hash.
+
+