diff --git a/services/crates/crates-base.js b/services/crates/crates-base.js
index 079f519de54826d9aa80a93d1cfeede537822519..7b7571c55d029b95b248a2e48838ba77e1912960 100644
--- a/services/crates/crates-base.js
+++ b/services/crates/crates-base.js
@@ -4,6 +4,8 @@ import { BaseJsonService, InvalidResponse } from '../index.js'
 
 const versionSchema = Joi.object({
   downloads: nonNegativeInteger,
+  // Crate size is not available for all versions.
+  crate_size: nonNegativeInteger.allow(null),
   num: Joi.string().required(),
   license: Joi.string().required().allow(null),
   rust_version: Joi.string().allow(null),
diff --git a/services/crates/crates-downloads.tester.js b/services/crates/crates-downloads.tester.js
index 8bc169570e5fbbb1daa87c754a0ed2b6d75303c8..1f2986e722e134b661c074f5cd243b89dd3f1208 100644
--- a/services/crates/crates-downloads.tester.js
+++ b/services/crates/crates-downloads.tester.js
@@ -42,7 +42,12 @@ t.create('recent downloads (null)')
           max_version: '0.2.71',
         },
         versions: [
-          { downloads: 42, license: 'MIT OR Apache-2.0', num: '0.2.71' },
+          {
+            downloads: 42,
+            license: 'MIT OR Apache-2.0',
+            num: '0.2.71',
+            crate_size: 42,
+          },
         ],
       }),
   )
diff --git a/services/crates/crates-size.service.js b/services/crates/crates-size.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..ab819c46ac4ae48012bfa74e82b561ff525d7018
--- /dev/null
+++ b/services/crates/crates-size.service.js
@@ -0,0 +1,59 @@
+import prettyBytes from 'pretty-bytes'
+import { InvalidResponse, pathParams } from '../index.js'
+import { BaseCratesService, description } from './crates-base.js'
+
+export default class CratesSize extends BaseCratesService {
+  static category = 'size'
+  static route = {
+    base: 'crates/size',
+    pattern: ':crate/:version?',
+  }
+
+  static openApi = {
+    '/crates/size/{crate}': {
+      get: {
+        summary: 'Crates.io Size',
+        description,
+        parameters: pathParams({
+          name: 'crate',
+          example: 'rustc-serialize',
+        }),
+      },
+    },
+    '/crates/size/{crate}/{version}': {
+      get: {
+        summary: 'Crates.io Size (version)',
+        description,
+        parameters: pathParams(
+          {
+            name: 'crate',
+            example: 'rustc-serialize',
+          },
+          {
+            name: 'version',
+            example: '0.3.24',
+          },
+        ),
+      },
+    },
+  }
+
+  render({ size }) {
+    return {
+      label: 'size',
+      message: prettyBytes(size),
+      color: 'blue',
+    }
+  }
+
+  async handle({ crate, version }) {
+    const json = await this.fetch({ crate, version })
+    const size = this.constructor.getVersionObj(json).crate_size
+
+    if (size == null) {
+      throw new InvalidResponse({ prettyMessage: 'unknown' })
+    }
+
+    return this.render({ size })
+  }
+}
diff --git a/services/crates/crates-size.tester.js b/services/crates/crates-size.tester.js
new file mode 100644
index 0000000000000000000000000000000000000000..6aea268ced66d473bdd8ddfae817d829591ed58b
--- /dev/null
+++ b/services/crates/crates-size.tester.js
@@ -0,0 +1,19 @@
+import { createServiceTester } from '../tester.js'
+import { isFileSize } from '../test-validators.js'
+export const t = await createServiceTester()
+
+t.create('size')
+  .get('/tokio.json')
+  .expectBadge({ label: 'size', message: isFileSize })
+
+t.create('size (with version)')
+  .get('/tokio/1.32.0.json')
+  .expectBadge({ label: 'size', message: '725 kB' })
+
+t.create('size (with version where version doesnt have size)')
+  .get('/tokio/0.1.6.json')
+  .expectBadge({ label: 'crates.io', message: 'unknown' })
+
+t.create('size (not found)')
+  .get('/not-a-crate.json')
+  .expectBadge({ label: 'crates.io', message: 'not found' })