diff --git a/lib/manager/cargo/__snapshots__/artifacts.spec.ts.snap b/lib/manager/cargo/__snapshots__/artifacts.spec.ts.snap
index 7e6b944d3ccdcafd27bc3df998212ac21ff50d00..4c0587bb0020975a44fa35a4ff2bf907f02c1c34 100644
--- a/lib/manager/cargo/__snapshots__/artifacts.spec.ts.snap
+++ b/lib/manager/cargo/__snapshots__/artifacts.spec.ts.snap
@@ -114,3 +114,26 @@ Array [
   },
 ]
 `;
+
+exports[`.updateArtifacts() returns updated workspace Cargo.lock 1`] = `
+Array [
+  Object {
+    "cmd": "cargo update --manifest-path crates/one/Cargo.toml --package dep1",
+    "options": Object {
+      "cwd": "/tmp/github/some/repo",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+]
+`;
diff --git a/lib/manager/cargo/artifacts.spec.ts b/lib/manager/cargo/artifacts.spec.ts
index 537d324b27a17f181a3157c2d7b322e77d5775aa..50bba66fa8bdc5e86aa27422f5cad1525b25f6ad 100644
--- a/lib/manager/cargo/artifacts.spec.ts
+++ b/lib/manager/cargo/artifacts.spec.ts
@@ -35,6 +35,7 @@ describe('.updateArtifacts()', () => {
     docker.resetPrefetchedImages();
   });
   it('returns null if no Cargo.lock found', async () => {
+    fs.stat.mockRejectedValue(new Error('not found!'));
     const updatedDeps = ['dep1'];
     expect(
       await cargo.updateArtifacts({
@@ -56,9 +57,11 @@ describe('.updateArtifacts()', () => {
     ).toBeNull();
   });
   it('returns null if unchanged', async () => {
+    fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any);
     fs.readFile.mockResolvedValueOnce('Current Cargo.lock' as any);
     const execSnapshots = mockExecAll(exec);
     fs.readFile.mockResolvedValueOnce('Current Cargo.lock' as any);
+
     const updatedDeps = ['dep1'];
     expect(
       await cargo.updateArtifacts({
@@ -71,6 +74,7 @@ describe('.updateArtifacts()', () => {
     expect(execSnapshots).toMatchSnapshot();
   });
   it('returns updated Cargo.lock', async () => {
+    fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any);
     git.getFile.mockResolvedValueOnce('Old Cargo.lock');
     const execSnapshots = mockExecAll(exec);
     fs.readFile.mockResolvedValueOnce('New Cargo.lock' as any);
@@ -86,7 +90,28 @@ describe('.updateArtifacts()', () => {
     expect(execSnapshots).toMatchSnapshot();
   });
 
+  it('returns updated workspace Cargo.lock', async () => {
+    fs.stat.mockRejectedValueOnce(new Error('crates/one/Cargo.lock not found'));
+    fs.stat.mockRejectedValueOnce(new Error('crates/Cargo.lock not found'));
+    fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any);
+
+    git.getFile.mockResolvedValueOnce('Old Cargo.lock');
+    const execSnapshots = mockExecAll(exec);
+    fs.readFile.mockResolvedValueOnce('New Cargo.lock' as any);
+    const updatedDeps = ['dep1'];
+    expect(
+      await cargo.updateArtifacts({
+        packageFileName: 'crates/one/Cargo.toml',
+        updatedDeps,
+        newPackageFileContent: '{}',
+        config,
+      })
+    ).not.toBeNull();
+    expect(execSnapshots).toMatchSnapshot();
+  });
+
   it('returns updated Cargo.lock for lockfile maintenance', async () => {
+    fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any);
     git.getFile.mockResolvedValueOnce('Old Cargo.lock');
     const execSnapshots = mockExecAll(exec);
     fs.readFile.mockResolvedValueOnce('New Cargo.lock' as any);
@@ -102,6 +127,7 @@ describe('.updateArtifacts()', () => {
   });
 
   it('returns updated Cargo.lock with docker', async () => {
+    fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any);
     jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
     await setExecConfig({ ...config, binarySource: BinarySource.Docker });
     git.getFile.mockResolvedValueOnce('Old Cargo.lock');
@@ -119,6 +145,7 @@ describe('.updateArtifacts()', () => {
     expect(execSnapshots).toMatchSnapshot();
   });
   it('catches errors', async () => {
+    fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any);
     fs.readFile.mockResolvedValueOnce('Current Cargo.lock' as any);
     fs.outputFile.mockImplementationOnce(() => {
       throw new Error('not found');
diff --git a/lib/manager/cargo/artifacts.ts b/lib/manager/cargo/artifacts.ts
index 9d08e90dbac3efaf4c51fbf60ab8b1c4e735a65b..857a61b8522376a98d318e10b580dabf7912680e 100644
--- a/lib/manager/cargo/artifacts.ts
+++ b/lib/manager/cargo/artifacts.ts
@@ -2,7 +2,7 @@ import { quote } from 'shlex';
 import { logger } from '../../logger';
 import { ExecOptions, exec } from '../../util/exec';
 import {
-  getSiblingFileName,
+  findLocalSiblingOrParent,
   readLocalFile,
   writeLocalFile,
 } from '../../util/fs';
@@ -64,8 +64,16 @@ export async function updateArtifacts({
     return null;
   }
 
-  const lockFileName = getSiblingFileName(packageFileName, 'Cargo.lock');
-  const existingLockFileContent = await readLocalFile(lockFileName);
+  // For standalone package crates, the `Cargo.lock` will be in the same
+  // directory as `Cargo.toml` (ie. a sibling). For cargo workspaces, it
+  // will be further up.
+  const lockFileName = await findLocalSiblingOrParent(
+    packageFileName,
+    'Cargo.lock'
+  );
+  const existingLockFileContent = lockFileName
+    ? await readLocalFile(lockFileName)
+    : null;
   if (!existingLockFileContent) {
     logger.debug('No Cargo.lock found');
     return null;
diff --git a/lib/util/fs/index.spec.ts b/lib/util/fs/index.spec.ts
index 7614d83516cf6ff1532b162847157ede40755528..81fe579b984dd3468bb237334374b5adf144d3b9 100644
--- a/lib/util/fs/index.spec.ts
+++ b/lib/util/fs/index.spec.ts
@@ -1,5 +1,13 @@
+import { withDir } from 'tmp-promise';
 import { getName } from '../../../test/util';
-import { getSubDirectory, localPathExists, readLocalFile } from '.';
+import {
+  findLocalSiblingOrParent,
+  getSubDirectory,
+  localPathExists,
+  readLocalFile,
+  setFsConfig,
+  writeLocalFile,
+} from '.';
 
 describe(getName(__filename), () => {
   describe('readLocalFile', () => {
@@ -32,3 +40,59 @@ describe(getName(__filename), () => {
     });
   });
 });
+
+describe(getName(__filename), () => {
+  describe('findLocalSiblingOrParent', () => {
+    it('returns path for file', async () => {
+      await withDir(
+        async (localDir) => {
+          setFsConfig({
+            localDir: localDir.path,
+          });
+
+          await writeLocalFile('crates/one/Cargo.toml', '');
+          await writeLocalFile('Cargo.lock', '');
+
+          expect(
+            await findLocalSiblingOrParent(
+              'crates/one/Cargo.toml',
+              'Cargo.lock'
+            )
+          ).toBe('Cargo.lock');
+          expect(
+            await findLocalSiblingOrParent(
+              'crates/one/Cargo.toml',
+              'Cargo.mock'
+            )
+          ).toBeNull();
+
+          await writeLocalFile('crates/one/Cargo.lock', '');
+
+          expect(
+            await findLocalSiblingOrParent(
+              'crates/one/Cargo.toml',
+              'Cargo.lock'
+            )
+          ).toBe('crates/one/Cargo.lock');
+          expect(
+            await findLocalSiblingOrParent('crates/one', 'Cargo.lock')
+          ).toBe('Cargo.lock');
+          expect(
+            await findLocalSiblingOrParent(
+              'crates/one/Cargo.toml',
+              'Cargo.mock'
+            )
+          ).toBeNull();
+        },
+        {
+          unsafeCleanup: true,
+        }
+      );
+    });
+
+    it('immediately returns null when either path is absolute', async () => {
+      expect(await findLocalSiblingOrParent('/etc/hosts', 'other')).toBeNull();
+      expect(await findLocalSiblingOrParent('other', '/etc/hosts')).toBeNull();
+    });
+  });
+});
diff --git a/lib/util/fs/index.ts b/lib/util/fs/index.ts
index da73e844283bc3b590a64f1d1b624cf516694737..d94cef1f8420aaac0cb40a35f189f7f5c265207a 100644
--- a/lib/util/fs/index.ts
+++ b/lib/util/fs/index.ts
@@ -1,5 +1,5 @@
 import * as fs from 'fs-extra';
-import { join, parse } from 'upath';
+import { isAbsolute, join, parse } from 'upath';
 import { RenovateConfig } from '../../config/common';
 import { logger } from '../../logger';
 
@@ -96,3 +96,32 @@ export function localPathExists(pathName: string): Promise<boolean> {
     .then((s) => !!s)
     .catch(() => false);
 }
+
+/**
+ * Tries to find `otherFileName` in the directory where
+ * `existingFileNameWithPath` is, then in its parent directory, then in the
+ * grandparent, until we reach the top-level directory. All paths
+ * must be relative to `localDir`.
+ */
+export async function findLocalSiblingOrParent(
+  existingFileNameWithPath: string,
+  otherFileName: string
+): Promise<string | null> {
+  if (isAbsolute(existingFileNameWithPath)) {
+    return null;
+  }
+  if (isAbsolute(otherFileName)) {
+    return null;
+  }
+
+  let current = existingFileNameWithPath;
+  while (current !== '') {
+    current = getSubDirectory(current);
+    const candidate = join(current, otherFileName);
+    if (await localPathExists(candidate)) {
+      return candidate;
+    }
+  }
+
+  return null;
+}