Capping Memory with cgroups v2 on an Oversubscribed VPS

  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

    • Assuming memory.max caps RSS. It caps the cgroup’s total memory including page cache charged to it. A process reading a 300 MB file may hit its limit with a small RSS.
    • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
    • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
    • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
    • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.

    Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

    • Assuming memory.max caps RSS. It caps the cgroup’s total memory including page cache charged to it. A process reading a 300 MB file may hit its limit with a small RSS.
    • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
    • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
    • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
    • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.

    Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

    • Assuming memory.max caps RSS. It caps the cgroup’s total memory including page cache charged to it. A process reading a 300 MB file may hit its limit with a small RSS.
    • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
    • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
    • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
    • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.

    Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

    • Assuming memory.max caps RSS. It caps the cgroup’s total memory including page cache charged to it. A process reading a 300 MB file may hit its limit with a small RSS.
    • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
    • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
    • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
    • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.

    Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

    • Assuming memory.max caps RSS. It caps the cgroup’s total memory including page cache charged to it. A process reading a 300 MB file may hit its limit with a small RSS.
    • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
    • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
    • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
    • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.

    Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

    • Assuming memory.max caps RSS. It caps the cgroup’s total memory including page cache charged to it. A process reading a 300 MB file may hit its limit with a small RSS.
    • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
    • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
    • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
    • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.

    Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

    • Assuming memory.max caps RSS. It caps the cgroup’s total memory including page cache charged to it. A process reading a 300 MB file may hit its limit with a small RSS.
    • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
    • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
    • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
    • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.

    Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

    • Assuming memory.max caps RSS. It caps the cgroup’s total memory including page cache charged to it. A process reading a 300 MB file may hit its limit with a small RSS.
    • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
    • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
    • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
    • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.

    Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

    • Assuming memory.max caps RSS. It caps the cgroup’s total memory including page cache charged to it. A process reading a 300 MB file may hit its limit with a small RSS.
    • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
    • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
    • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
    • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.

    Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

    • Assuming memory.max caps RSS. It caps the cgroup’s total memory including page cache charged to it. A process reading a 300 MB file may hit its limit with a small RSS.
    • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
    • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
    • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
    • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.

    Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

    • Assuming memory.max caps RSS. It caps the cgroup’s total memory including page cache charged to it. A process reading a 300 MB file may hit its limit with a small RSS.
    • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
    • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
    • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
    • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.

    Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

    On a VPS with 1 GB of RAM you have two blunt instruments for memory pressure: point your applications at sane limits and hope, or let the kernel OOM killer decide. Neither is satisfying, and the OOM killer’s choice is frequently not the process you would have picked. cgroups v2 gives you a third option — a hard, enforceable ceiling on any process tree, with the OOM event delivered to that cgroup instead of the whole system. This article covers setting that up from scratch on a Debian 12 / Ubuntu 24.04 VPS, and the differences from cgroups v1 that catch people out.

    Before capping anything, it helps to know what your workload’s real working set is. Our guide to reading load average versus actual CPU saturation explains why memory pressure and load average tell different stories on a shared VPS.

    Confirm you are on cgroups v2

    stat -fc %T /sys/fs/cgroup

    cgroup2fs means unified v2. tmpfs means v1 and this article’s paths do not apply. Check the controller list:

    cat /sys/fs/cgroup/cgroup.controllers

    You should see at least cpu io memory pids. On Ubuntu the memory controller is available at the root but must be enabled in child cgroups by writing to cgroup.subtree_control. Debian 12 with a recent kernel behaves the same.

    The three memory knobs that matter

    FileTypeBehaviourTypical use
    memory.maxHard limitAllocation triggers reclaim; if reclaim fails, cgroup OOM killer firesHard ceiling for an app that must not starve the host
    memory.highSoft limitThrottles allocation and reclaims aggressively, no OOMPreferred: gives the app a chance to shed load
    memory.minProtectionPages here are never reclaimed under pressureProtect a critical daemon in a shared cgroup tree
    memory.swap.maxHard limitCaps swap the cgroup may useStop one service from swapping the whole box
    memory.lowProtection (soft)Reclaim avoids this range where possibleSofter alternative to memory.min
    memory.oom.groupToggleKill the entire cgroup instead of one processMulti-process apps that corrupt state if half-killed

    The practical advice: set memory.high at roughly 90% of your intended ceiling and memory.max at 100%. The high watermark throttles and reclaims gradually, which most applications survive; the max watermark is the backstop that prevents a runaway from consuming the host.

    Cap a service manually with systemd

    systemd already creates a cgroup per unit and exposes the controls as directives. This is the least error-prone route and needs no manual filesystem work. Create a drop-in:

    systemctl edit php8.3-fpm.service

    [Service]
    MemoryHigh=460M
    MemoryMax=512M
    MemorySwapMax=128M
    MemoryAccounting=yes
    TasksMax=256

    systemctl daemon-reload && systemctl restart php8.3-fpm.service

    Verify:

    systemctl show php8.3-fpm.service -p MemoryCurrent -p MemoryMax -p MemoryHigh
    cat /sys/fs/cgroup/system.slice/php8.3-fpm.service/memory.current

    Note that MemoryCurrent is in bytes and reports [not set] if MemoryAccounting= is off. Always set accounting explicitly.

    Cap an arbitrary process tree with systemd-run

    For something not managed by a unit — a batch job, a scraper, a build — wrap it:

    systemd-run --scope --unit=batchjob --property=MemoryMax=384M --property=MemorySwapMax=0 \
      /usr/local/bin/nightly-indexer.sh

    --scope runs the process in the current session but still inside a cgroup, so the cap applies and it appears in systemd-cgtop. If the job exceeds 384 MB it is killed by its own cgroup OOM, and the rest of the VPS is unaffected. This is by far the simplest way to stop a one-off job from taking down your database.

    Watching pressure and OOM events

    Every cgroup exposes its own pressure stall information. For a cgroup at /sys/fs/cgroup/system.slice/php8.3-fpm.service/:

    cat memory.pressure
    cat memory.events

    memory.events counts high, max, oom and oom_kill occurrences since boot. A rising high count with zero oom_kill is exactly what you want: the throttle is working. Non-zero oom_kill means the ceiling is genuinely too low for the workload, not that tuning is needed.

    systemd-cgtop -m --order=memory gives a live ranked view, which is usually faster than reading the files by hand when you suspect a leak.

    A worked oversubscription plan for a 1 GB VPS

    Assume 1 GB total, ~120 MB consumed by the kernel and base system. That leaves roughly 880 MB to allocate. Deliberately oversubscribe the soft limits (they can borrow when idle) but keep the hard limits within the physical budget.

    Servicememory.highmemory.maxswap.maxRationale
    MariaDB320M380M64MBuffer pool is fixed; small headroom for connections
    PHP-FPM200M240M64MScales with children; throttle before OOM
    Nginx24M48M0Tiny, should never swap
    Redis64M96M0Set maxmemory in Redis too
    System / sshd / cron96M128M32MProtected with memory.min
    Total hard max892M160MFits in RAM, swap is a cushion only

    Protect the system slice so SSH stays reachable even when MariaDB is reclaiming hard:

    echo 67108864 > /sys/fs/cgroup/system.slice/memory.min

    Persist that with a drop-in on system.slice or, more practically, accept the systemd defaults and instead make sure your application limits are the ones being hit first. Setting a memory.min on the system slice is a blunt safety net and worth doing once you have been OOM-killed out of an SSH session during an incident.

    Common mistakes

  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

  • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
  • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
  • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
  • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.
  • Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

    • Assuming memory.max caps RSS. It caps the cgroup’s total memory including page cache charged to it. A process reading a 300 MB file may hit its limit with a small RSS.
    • Setting only memory.max. Without memory.high you lose the graceful throttle and go straight to a kill.
    • Forgetting swap. memory.swap.max=0 on a database is often correct, but on a stateless web worker it turns manageable pressure into a hard kill.
    • Ignoring nested cgroups. A limit on a parent slice and a child unit both apply; the more restrictive one wins. Check the full path with systemd-cgls.
    • Trusting old guides. Anything referencing memory.limit_in_bytes, tasks or cgroup.clone_children is cgroups v1 and will not work on a modern VPS.

    Where this fits

    cgroup memory limits do not make a workload fit in less RAM. They make the failure mode predictable: instead of a global OOM that takes out MariaDB because it happened to be the largest RSS, you get a scoped kill of the process that actually misbehaved, and a counter in memory.events telling you it happened. On an oversubscribed VPS that is the difference between a degraded service and a dead one. Pair it with correct application-level sizing — see our PHP-FPM pool sizing guide — and you have a system that fails locally instead of globally.

    Leave a Reply