diff --git a/README.md b/README.md
index 9a1d93e..610459a 100644
--- a/README.md
+++ b/README.md
@@ -233,6 +233,22 @@ if (fresh) {
 }
 ```
 
+### `getMetadata(key: string, { etag?: string, type?: string }): Promise<{ data: any, etag: string, metadata: object }>`
+
+Retrieves any metadata associated with a given key and its
+[ETag value](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag).
+
+If an object with the given key is not found, `null` is returned.
+
+This method can be used to check whether a key exists without having to actually retrieve it and transfer a
+potentially-large blob.
+
+```javascript
+const blob = await store.getMetadata('some-key')
+
+console.log(blob.etag, blob.metadata)
+```
+
 ### `set(key: string, value: ArrayBuffer | Blob | ReadableStream | string, { metadata?: object }): Promise<void>`
 
 Creates an object with the given key and value.
diff --git a/src/client.ts b/src/client.ts
index 5c8a4a8..febd49d 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -84,6 +84,8 @@ export class Client {
 
     url.searchParams.set('context', storeName)
 
+    // If there is no key, we're dealing with the list endpoint, which is
+    // implemented directly in the Netlify API.
     if (key === undefined) {
       return {
         headers: apiHeaders,
@@ -97,6 +99,14 @@ export class Client {
       apiHeaders[METADATA_HEADER_EXTERNAL] = encodedMetadata
     }
 
+    // HEAD requests are implemented directly in the Netlify API.
+    if (method === HTTPMethod.HEAD) {
+      return {
+        headers: apiHeaders,
+        url: url.toString(),
+      }
+    }
+
     const res = await this.fetch(url.toString(), { headers: apiHeaders, method })
 
     if (res.status !== 200) {
diff --git a/src/main.test.ts b/src/main.test.ts
index a54673d..0f56712 100644
--- a/src/main.test.ts
+++ b/src/main.test.ts
@@ -346,6 +346,120 @@ describe('get', () => {
   })
 })
 
+describe('getMetadata', () => {
+  describe('With API credentials', () => {
+    test('Reads from the blob store and returns the etag and the metadata object', async () => {
+      const mockMetadata = {
+        name: 'Netlify',
+        cool: true,
+        functions: ['edge', 'serverless'],
+      }
+      const headers = {
+        etag: '123456789',
+        'x-amz-meta-user': `b64;${base64Encode(mockMetadata)}`,
+      }
+      const mockStore = new MockFetch().head({
+        headers: { authorization: `Bearer ${apiToken}` },
+        response: new Response(null, { headers }),
+        url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`,
+      })
+
+      globalThis.fetch = mockStore.fetch
+
+      const blobs = getStore({
+        name: 'production',
+        token: apiToken,
+        siteID,
+      })
+
+      const entry = await blobs.getMetadata(key)
+      expect(entry?.etag).toBe(headers.etag)
+      expect(entry?.metadata).toEqual(mockMetadata)
+
+      expect(mockStore.fulfilled).toBeTruthy()
+    })
+
+    test('Returns `null` when the API returns a 404', async () => {
+      const mockStore = new MockFetch().head({
+        headers: { authorization: `Bearer ${apiToken}` },
+        response: new Response(null, { status: 404 }),
+        url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`,
+      })
+
+      globalThis.fetch = mockStore.fetch
+
+      const blobs = getStore({
+        name: 'production',
+        token: apiToken,
+        siteID,
+      })
+
+      expect(await blobs.getMetadata(key)).toBeNull()
+      expect(mockStore.fulfilled).toBeTruthy()
+    })
+
+    test('Throws when the metadata object cannot be parsed', async () => {
+      const headers = {
+        etag: '123456789',
+        'x-amz-meta-user': `b64;${base64Encode(`{"name": "Netlify", "cool`)}`,
+      }
+      const mockStore = new MockFetch().head({
+        headers: { authorization: `Bearer ${apiToken}` },
+        response: new Response(null, { headers }),
+        url: `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`,
+      })
+
+      globalThis.fetch = mockStore.fetch
+
+      const blobs = getStore({
+        name: 'production',
+        token: apiToken,
+        siteID,
+      })
+
+      await expect(async () => await blobs.getMetadata(key)).rejects.toThrowError(
+        'An internal error occurred while trying to retrieve the metadata for an entry. Please try updating to the latest version of the Netlify Blobs client.',
+      )
+
+      expect(mockStore.fulfilled).toBeTruthy()
+    })
+  })
+
+  describe('With edge credentials', () => {
+    test('Reads from the blob store and returns the etag and the metadata object', async () => {
+      const mockMetadata = {
+        name: 'Netlify',
+        cool: true,
+        functions: ['edge', 'serverless'],
+      }
+      const headers = {
+        etag: '123456789',
+        'x-amz-meta-user': `b64;${base64Encode(mockMetadata)}`,
+      }
+      const mockStore = new MockFetch().head({
+        headers: { authorization: `Bearer ${edgeToken}` },
+        response: new Response(null, { headers }),
+        url: `${edgeURL}/${siteID}/production/${key}`,
+      })
+
+      globalThis.fetch = mockStore.fetch
+
+      const blobs = getStore({
+        edgeURL,
+        name: 'production',
+        token: edgeToken,
+        siteID,
+      })
+
+      const entry = await blobs.getMetadata(key)
+      expect(entry?.etag).toBe(headers.etag)
+      expect(entry?.metadata).toEqual(mockMetadata)
+
+      expect(mockStore.fulfilled).toBeTruthy()
+    })
+  })
+})
+
 describe('getWithMetadata', () => {
   describe('With API credentials', () => {
     test('Reads from the blob store and returns the etag and the metadata object', async () => {
@@ -387,14 +501,14 @@ describe('getWithMetadata', () => {
       })
 
       const entry1 = await blobs.getWithMetadata(key)
-      expect(entry1.data).toBe(value)
-      expect(entry1.etag).toBe(responseHeaders.etag)
-      expect(entry1.metadata).toEqual(mockMetadata)
+      expect(entry1?.data).toBe(value)
+      expect(entry1?.etag).toBe(responseHeaders.etag)
+      expect(entry1?.metadata).toEqual(mockMetadata)
 
       const entry2 = await blobs.getWithMetadata(key, { type: 'stream' })
-      expect(await streamToString(entry2.data as unknown as NodeJS.ReadableStream)).toBe(value)
-      expect(entry2.etag).toBe(responseHeaders.etag)
-      expect(entry2.metadata).toEqual(mockMetadata)
+      expect(await streamToString(entry2?.data as unknown as NodeJS.ReadableStream)).toBe(value)
+      expect(entry2?.etag).toBe(responseHeaders.etag)
+      expect(entry2?.metadata).toEqual(mockMetadata)
 
       expect(mockStore.fulfilled).toBeTruthy()
     })
@@ -495,16 +609,16 @@ describe('getWithMetadata', () => {
       })
 
       const staleEntry = await blobs.getWithMetadata(key, { etag: etags[0] })
-      expect(staleEntry.data).toBe(value)
-      expect(staleEntry.etag).toBe(etags[0])
-      expect(staleEntry.fresh).toBe(false)
-      expect(staleEntry.metadata).toEqual(mockMetadata)
+      expect(staleEntry?.data).toBe(value)
+      expect(staleEntry?.etag).toBe(etags[0])
+      expect(staleEntry?.fresh).toBe(false)
+      expect(staleEntry?.metadata).toEqual(mockMetadata)
 
       const freshEntry = await blobs.getWithMetadata(key, { etag: etags[1], type: 'text' })
-      expect(freshEntry.data).toBe(null)
-      expect(freshEntry.etag).toBe(etags[0])
-      expect(freshEntry.fresh).toBe(true)
-      expect(freshEntry.metadata).toEqual(mockMetadata)
+      expect(freshEntry?.data).toBe(null)
+      expect(freshEntry?.etag).toBe(etags[0])
+      expect(freshEntry?.fresh).toBe(true)
+      expect(freshEntry?.metadata).toEqual(mockMetadata)
 
       expect(mockStore.fulfilled).toBeTruthy()
     })
@@ -543,14 +657,14 @@ describe('getWithMetadata', () => {
       })
 
       const entry1 = await blobs.getWithMetadata(key)
-      expect(entry1.data).toBe(value)
-      expect(entry1.etag).toBe(responseHeaders.etag)
-      expect(entry1.metadata).toEqual(mockMetadata)
+      expect(entry1?.data).toBe(value)
+      expect(entry1?.etag).toBe(responseHeaders.etag)
+      expect(entry1?.metadata).toEqual(mockMetadata)
 
       const entry2 = await blobs.getWithMetadata(key, { type: 'stream' })
-      expect(await streamToString(entry2.data as unknown as NodeJS.ReadableStream)).toBe(value)
-      expect(entry2.etag).toBe(responseHeaders.etag)
-      expect(entry2.metadata).toEqual(mockMetadata)
+      expect(await streamToString(entry2?.data as unknown as NodeJS.ReadableStream)).toBe(value)
+      expect(entry2?.etag).toBe(responseHeaders.etag)
+      expect(entry2?.metadata).toEqual(mockMetadata)
 
       expect(mockStore.fulfilled).toBeTruthy()
     })
diff --git a/src/metadata.ts b/src/metadata.ts
index 288a70d..0f80404 100644
--- a/src/metadata.ts
+++ b/src/metadata.ts
@@ -33,3 +33,17 @@ export const decodeMetadata = (header: string | null): Metadata => {
 
   return metadata
 }
+
+export const getMetadataFromResponse = (response: Response) => {
+  if (!response.headers) {
+    return {}
+  }
+
+  try {
+    return decodeMetadata(response.headers.get(METADATA_HEADER_INTERNAL))
+  } catch {
+    throw new Error(
+      'An internal error occurred while trying to retrieve the metadata for an entry. Please try updating to the latest version of the Netlify Blobs client.',
+    )
+  }
+}
diff --git a/src/server.test.ts b/src/server.test.ts
index 4ac564c..74b9b7d 100644
--- a/src/server.test.ts
+++ b/src/server.test.ts
@@ -62,7 +62,7 @@ test('Reads and writes from the file system', async () => {
   expect(await blobs.get('parent')).toBe(null)
 
   const entry = await blobs.getWithMetadata('simple-key')
-  expect(entry.metadata).toEqual(metadata)
+  expect(entry?.metadata).toEqual(metadata)
 
   await blobs.delete('simple-key')
   expect(await blobs.get('simple-key')).toBe(null)
diff --git a/src/store.ts b/src/store.ts
index 10b732d..d594d0e 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -2,7 +2,7 @@ import { Buffer } from 'node:buffer'
 
 import { ListResponse, ListResponseBlob } from './backend/list.ts'
 import { Client } from './client.ts'
-import { decodeMetadata, Metadata, METADATA_HEADER_INTERNAL } from './metadata.ts'
+import { getMetadataFromResponse, Metadata } from './metadata.ts'
 import { BlobInput, HTTPMethod } from './types.ts'
 import { BlobsInternalError } from './util.ts'
 
@@ -131,10 +131,31 @@ export class Store {
     throw new BlobsInternalError(res.status)
   }
 
+  async getMetadata(key: string) {
+    const res = await this.client.makeRequest({ key, method: HTTPMethod.HEAD, storeName: this.name })
+
+    if (res.status === 404) {
+      return null
+    }
+
+    if (res.status !== 200 && res.status !== 304) {
+      throw new BlobsInternalError(res.status)
+    }
+
+    const etag = res?.headers.get('etag') ?? undefined
+    const metadata = getMetadataFromResponse(res)
+    const result = {
+      etag,
+      metadata,
+    }
+
+    return result
+  }
+
   async getWithMetadata(
     key: string,
     options?: GetWithMetadataOptions,
-  ): Promise<{ data: string } & GetWithMetadataResult>
+  ): Promise<({ data: string } & GetWithMetadataResult) | null>
 
   async getWithMetadata(
     key: string,
@@ -144,26 +165,26 @@ export class Store {
   async getWithMetadata(
     key: string,
     options: { type: 'blob' } & GetWithMetadataOptions,
-  ): Promise<{ data: Blob } & GetWithMetadataResult>
+  ): Promise<({ data: Blob } & GetWithMetadataResult) | null>
 
   /* eslint-disable @typescript-eslint/no-explicit-any */
 
   async getWithMetadata(
     key: string,
     options: { type: 'json' } & GetWithMetadataOptions,
-  ): Promise<{ data: any } & GetWithMetadataResult>
+  ): Promise<({ data: any } & GetWithMetadataResult) | null>
 
   /* eslint-enable @typescript-eslint/no-explicit-any */
 
   async getWithMetadata(
     key: string,
     options: { type: 'stream' } & GetWithMetadataOptions,
-  ): Promise<{ data: ReadableStream } & GetWithMetadataResult>
+  ): Promise<({ data: ReadableStream } & GetWithMetadataResult) | null>
 
   async getWithMetadata(
     key: string,
     options: { type: 'text' } & GetWithMetadataOptions,
-  ): Promise<{ data: string } & GetWithMetadataResult>
+  ): Promise<({ data: string } & GetWithMetadataResult) | null>
 
   async getWithMetadata(
     key: string,
@@ -187,17 +208,7 @@ export class Store {
     }
 
     const responseETag = res?.headers.get('etag') ?? undefined
-
-    let metadata: Metadata = {}
-
-    try {
-      metadata = decodeMetadata(res?.headers.get(METADATA_HEADER_INTERNAL))
-    } catch {
-      throw new Error(
-        'An internal error occurred while trying to retrieve the metadata for an entry. Please try updating to the latest version of the Netlify Blobs client.',
-      )
-    }
-
+    const metadata = getMetadataFromResponse(res)
     const result: GetWithMetadataResult = {
       etag: responseETag,
       fresh: false,
diff --git a/src/types.ts b/src/types.ts
index d8c45b9..dae0d3f 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -5,5 +5,6 @@ export type Fetcher = typeof globalThis.fetch
 export enum HTTPMethod {
   DELETE = 'delete',
   GET = 'get',
+  HEAD = 'head',
   PUT = 'put',
 }
diff --git a/test/mock_fetch.ts b/test/mock_fetch.ts
index 78e1aab..38c3739 100644
--- a/test/mock_fetch.ts
+++ b/test/mock_fetch.ts
@@ -45,6 +45,10 @@ export class MockFetch {
     return this.addExpectedRequest({ ...options, method: 'get' })
   }
 
+  head(options: ExpectedRequestOptions) {
+    return this.addExpectedRequest({ ...options, method: 'head' })
+  }
+
   post(options: ExpectedRequestOptions) {
     return this.addExpectedRequest({ ...options, method: 'post' })
   }